简介:基于协同过滤算法的私人诊所管理系统,是一套面向毕业设计、课程设计与Java开发学习者的完整工程源码,采用SpringBoot+Vue前后端分离架构,配合MySQL数据库实现。系统围绕私人诊所核心业务,包含个人中心、患者管理、医生管理、科室管理、出诊医生管理、预约挂号、预约取消、病历信息、药品信息、处方开具、留言板、系统管理等模块,并区分管理员、患者、医生三类角色,功能覆盖全面,可直接作为课题研究或项目二次开发的基础。资源包共633个文件,压缩包大小19.38MB,主要包含Java后端源码、Vue前端页面、SVG图标、SQL数据库脚本及开发文档等。sql文件可用于快速初始化数据库,docx/doc文档提供项目说明与设计参考,bat脚本便于一键启动项目,整体目录结构规范清晰,方便导入IDE运行调试,省去环境搭建和重复编码的时间。目前已有1893人学习下载。读者可结合协同过滤推荐机制,理解如何将推荐算法融入预约挂号或药品选择等实际管理场景,同时从预约取消、病历维护、处方开具等典型模块掌握系统分层设计与前后端交互实现思路,是一份兼顾毕业设计参考与全栈实战提升的优质资料。
1. 私人诊所的协同过滤落地:一份能跑的 springboot+vue 源码包怎么拆
私人诊所和大型医院最大的区别,是患者复诊率高、医生排班灵活、药品库存靠人工记。很多做毕业设计或者课程设计的同学拿到「基于协同过滤算法的私人诊所管理系统」这个题目,第一反应是「协同过滤四个字怎么写进 springboot 和 vue 里」,第二反应是「这份源码拿下来能不能直接跑起来当答辩演示」。这套资源就是围绕这个场景打包的:后端 springboot 做预约、病历、收费、药品管理,前端 vue 做医生排班看板和患者自助操作,中间再用协同过滤算法根据患者的历史就诊行为做医生推荐。适合两类人:一是需要完整毕设项目、不想从零搭框架的学生,二是想看看协同过滤在真实业务表结构里怎么落地的开发者。下面我按拆包顺序,把表结构、算法实现、运行配置和坑位一次讲完。
2. 系统架构与数据表设计:先看清表和接口再动手
2.1 前后端分层的模块划分
这套系统采用标准的 springboot + vue 前后端分离结构,后端负责业务逻辑和数据持久化,前端只做页面渲染和交互。拿到源码后第一步不是急着启动,而是先看目录结构,因为「能跑」和「跑对」是两件事。
后端常见分包是 controller、service、mapper、entity、config 五层。controller 层只做参数接收和结果封装,service 层写核心业务,mapper 层对接 MyBatis 操作数据库。config 层里放跨域配置、拦截器配置和 JWT 相关的初始化。注意这套系统里有一个独立的 recommender 包,协同过滤相关逻辑单独放,不跟普通 CRUD 混在一起,这一点设计得比较清楚——你答辩被问到算法时可以直接指到这个包讲解。
前端以 vue 单页应用为主,页面按角色拆:管理员端有医生管理、排班管理、收费统计;医生端有患者病历查看、处方录入;患者端有预约挂号、就诊记录、推荐医生展示。路由用 vue-router 控制,权限用登录接口返回的角色字段配合路由守卫做拦截。前后端通过 axios 调用接口,请求头里带 token。
2.2 数据库核心表与字段边界
SQL 文件是整个系统的地基。我导入后数了一下,核心业务表大概有七张:用户表、医生表、患者表、预约表、病历表、药品表、收费表。其中跟协同过滤直接相关的是预约表和病历表,因为推荐算法的输入数据要从这两张表里抽。
| 表名 | 核心字段 | 作用 |
|---|---|---|
| sys_user | id, username, password, role | 登录账号,区分管理员/医生/患者 |
| doctor | id, name, department, title | 医生基本信息,推荐结果输出的实体 |
| patient | id, user_id, name, phone | 患者信息,关联用户表 |
| appointment | id, patient_id, doctor_id, appoint_date, status | 预约记录,推荐算法的评分来源 |
| medical_record | id, patient_id, doctor_id, diagnosis, create_time | 病历记录,辅助推荐的特征 |
| medicine | id, name, stock, price | 药品库存与价格 |
| charge | id, patient_id, amount, charge_time | 收费记录,用于统计和报表 |
预约表里的 patient_id 和 doctor_id 是协同过滤的天然「用户-物品」关系。一个患者多次预约同一个医生,就相当于这个患者对这位医生有较高的隐式打分。病历表在推荐里可以当补充特征:比如某位患者被多位医生诊断为同一类病,那么它对同类科室的偏好程度可以从这里挖掘。
2.3 后端分层与前端页面映射
我习惯拿到项目后先画一张「接口-页面-表」的对应关系图,这样后面排查问题时能马上定位是前端传参错、后端逻辑错还是数据没落库。这套系统的主流程是:患者登录后看到医生列表,点击预约时向后端提交预约请求,预约成功后状态写入 appointment 表;患者再次登录时首页会多出一个推荐医生区域,这个区域的数据由推荐接口实时计算返回。
前端页面不多,主要文件在 src/views 下。登录页、首页、医生列表页、预约页、病历页、收费页,再加一个推荐页或推荐区块。路由配置里注意有一个 recommend 路由,它对应的是协同过滤的结果展示。这个路由可能是嵌套在首页里的子路由,也可能是独立页面,不同版本的源码会有差异,建议先全局搜一下 recommend 关键词。
3. 协同过滤算法实现:从评分矩阵到医生推荐
3.1 没有评分表怎么做协同过滤
很多同学拿到项目后翻数据库,发现没有一张 ratings 表,第一反应是「协同过滤的评分数据从哪来」。这是这套源码最容易让人困惑的地方。项目里没有显式评分表,用的是「行为替代评分」:患者每完成一次预约,就相当于给这位医生贡献了一次正反馈。预约次数越多,隐式评分越高。这种方案在真实业务里非常常见,因为让用户在医疗场景里打分很反直觉。
我用 SQL 做了一次聚合,得到「患者-医生」的就诊次数矩阵。结果长这样:每个 patient_id 对应多个 doctor_id 和对应的 count 值。这个 count 就是协同过滤的输入评分。要注意的是,count 只有非负值,所以这里用的是基于行为的隐式反馈,不是显式的 1-5 分评分。这导致相似度计算时不能用皮尔逊相关系数(它对未互动的 0 值很敏感),建议用余弦相似度。
SELECT patient_id, doctor_id, COUNT(*) AS score FROM appointment WHERE status = '已完成' GROUP BY patient_id, doctor_id;这段 SQL 把预约表里状态为已完成的记录按患者和医生分组统计次数,得到的就是后续算法要用的评分矩阵。注意状态条件很重要,如果计入已取消的记录,推荐结果会被无效数据污染。我在实际跑数据时发现,源项目里预约状态至少有两种:已预约和已完成,取已完成更能反映真实偏好。
3.2 相似度计算与推荐生成的 Java 实现
拿到评分矩阵后,下一步是算医生之间的相似度。这里用的是基于物品的协同过滤(ItemCF),思路是:如果很多患者同时预约过医生 A 和医生 B,那么 A 和 B 相似;给看过 A 的患者推荐 B 时,说服力就更强。计算余弦相似度的核心代码在 recommender 包里:
public double cosineSimilarity(Map<Integer, Double> vectorA, Map<Integer, Double> vectorB) { Set<Integer> commonUsers = new HashSet<>(vectorA.keySet()); commonUsers.retainAll(vectorB.keySet()); if (commonUsers.isEmpty()) { return 0.0; } double dotProduct = 0.0; double normA = 0.0; double normB = 0.0; for (Integer userId : commonUsers) { dotProduct += vectorA.get(userId) * vectorB.get(userId); } for (Double value : vectorA.values()) { normA += value * value; } for (Double value : vectorB.values()) { normB += value * value; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }这段代码先取两个医生共同被哪些患者预约过,再算点积和各自向量的模长。如果共同预约的患者为空,相似度直接返回 0,避免除零异常。实际计算时,vectorA 的 key 是患者 id,value 是该患者预约医生 A 的次数。对于小诊所的数据量,这个实现方式在性能上没有压力,不需要引入 Spark 之类的大数据组件。
3.3 推荐接口的加工与兜底策略
推荐接口的逻辑是:先找到跟目标患者历史上预约过相似医生的其他患者,再用他们的预约行为生成候选集,最后过滤掉目标患者已经预约过的医生。完整的加工步骤是:第一步算医生间相似度矩阵,第二步根据该患者的预约记录找出相似医生,第三步排序取 TopN,第四步兜底。
兜底策略在这个项目里很关键。如果某位患者是第一次来,没有任何预约记录,协同过滤直接返回空列表,前端推荐区域就会空白。源码里的兜底是:推荐该科室下预约热度最高的三位医生,热度用 appointment 表里按 doctor_id 分组统计的数量衡量。这个策略同样用 SQL 实现,不需要额外建表。
4. 环境搭建与运行:从 SQL 导入到前后端联调
4.1 版本选择:JDK、MySQL、Node 的具体搭配
这个项目对版本有隐性要求,直接用最新版可能翻车。我实际测试下来,JDK 1.8 最稳,springboot 版本如果在 2.x,JDK 8 完全兼容;如果不想折腾环境,以下版本组合是安全的:JDK 8、MySQL 5.7、Node 14 或 16、Maven 3.6。
MySQL 8.0 也能跑,但要注意驱动和时区设置。如果 pom 里用的是 mysql-connector-java 5.x 系列,而本机数据库是 8.0,连接会报 SSL 或时区相关的错误。遇到这种情况,优先把 MySQL 换成 5.7,或者升级驱动到 8.0 系列并加上serverTimezone=Asia/Shanghai参数。Node 版本不建议用 17 以上,vue-cli 3/4 在 Node 高版本下经常报 OpenSSL 的错误。
4.2 后端启动:数据源配置与 Maven 依赖
先把 SQL 文件导入数据库。我用命令行导入,比在图形工具里点导入更可控,避免字符集问题:
mysql -u root -p -D clinic_db < clinic_db.sql注意-D参数指定目标数据库名,clinic_db 是这套系统用的库名,具体以源码里 application.yml 的配置为准。导入后表名、字段名、初始化数据都是现成的。sql 文件里如果包含管理员账号和测试患者账号,先记下来,后面登录要用。
后端配置集中在 src/main/resources/application.yml:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/clinic_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这个配置里最容易改错的是数据库密码和 URL 里的库名。记得把 password 改成自己本机的 MySQL 密码,库名改成 SQL 导入时实际用的库名。mybatis 的map-underscore-to-camel-case建议保持 true,因为实体类用的是驼峰命名,而数据库字段是下划线风格,关掉后查询结果会大面积出现字段为 null 的情况。
4.3 前端启动:Vue 依赖安装与代理配置
前端目录下执行依赖安装,然后启动开发服务:
cd frontend npm install npm run serve如果 npm install 报错或者卡在某个包上,检查网络源是否稳定,可以临时切换到淘宝镜像源再装。启动成功后默认端口 8080 或 8081,具体看 vue.config.js 里的 devServer 配置。这里有个关键点:前端访问后端接口靠代理转发,代理目标必须跟后端 application.yml 里的服务端口一致。
// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };配好代理后,前端请求/api/login时,开发服务器会把请求转发到http://localhost:8080/api/login。如果代理没配错,但登录还是失败,先在前端浏览器开发者工具里看请求有没有真正发出去,再看 Network 面板里的状态码——404 是路径问题,401 是 token 问题,500 则优先查后端日志。
4.4 跑通一次完整业务流
环境都起来之后,我用管理员账号登录系统后台,把医生排班数据补齐,然后在测试患者账号下走一遍预约流程:患者登录 → 查看医生列表 → 点击预约 → 查看预约记录 → 查看推荐医生区域。推荐区域的数据不是历史预约过的医生,而是算法算出来的相似医生,看到这一块返回了数据,就说明协同过滤链路是通的。
如果推荐区域空白,优先检查 appointment 表里是否有已完成状态的记录。初始化 SQL 里通常会插入几十条预约记录做演示,如果被手动清掉了,推荐接口就没有输入数据。可以手动插入几条多患者预约同一医生的记录,再刷新页面验证。
5. 避坑记录:五条常见踩坑与排查路径
5.1 端口占用导致前后端连不上
现象:后端启动时报Port 8080 was already in use,前端页面所有接口请求全部失败。
原因:本机有其他程序占用了 8080 端口,或者之前启动的后端实例没有正常关闭。这种情况在开发机上非常常见,尤其是开过多个 springboot 项目后,关掉 IDE 但进程还挂在后台。
解决:在命令行执行netstat -ano | findstr 8080找到占用进程的 PID,再用taskkill /PID 对应PID /F杀掉。如果不想杀进程,改 application.yml 里的 server.port,同时把前端 vue.config.js 里的 target 改成新端口。注意两个地方的配置必须同步改,只改一处必然连不上。
5.2 MyBatis 的 XML 映射文件没被扫描
现象:后端启动正常,但调用列表接口时报Invalid bound statement (not found),或者 mapper 方法在运行时找不到对应的 SQL 语句。
原因:XML 文件放在 src/main/java 的 mapper 目录下,但 Maven 默认只编译 .java 文件,XML 没有被复制到 target/classes 里。很多同学直接在 IDE 里能看到 XML 文件,就以为编译后存在,其实打包时被忽略了。
解决:在 pom.xml 的 build 节点里加资源配置,让 Maven 打包时把 mapper XML 一并带上。
<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>如果你用的是 IDEA,配置对后还需要执行mvn clean再重启,否则旧的 target 目录里还是没有 XML 文件。这个坑在系统里很典型,因为推荐模块也依赖 MyBatis 查的预约数据,映射失败时推荐接口也会连带报错。
5.3 跨域配置和拦截器顺序问题
现象:前端请求能发出去,但浏览器报CORS policy: No 'Access-Control-Allow-Origin' header is present,登录接口也拿不到 token。
原因:springboot 后端配置了跨域允许,但 JWT 拦截器先执行了,放行逻辑没包含跨域预检请求 OPTIONS。预检请求被拦截后直接返回错误,浏览器看到没有跨域响应头就报错。这个顺序问题非常隐蔽,因为单独看跨域配置是好的,单独看拦截器也是好的,合在一起就断。
解决:在拦截器里先判断请求方法,如果是 OPTIONS 请求直接放行,不要走 token 校验。拦截器代码里加一个前置判断。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return true; } // 校验 token 逻辑 return true; }这段逻辑放在拦截器最前面,能避免绝大多数跨域与 JWT 组合场景下的诡异问题。我在排查时发现还顺带解决了「登录成功后第二步接口被拦截」的偶发问题,原因是 axios 请求头里的 token 在预检阶段就被吞掉了。
5.4 SQL 文件导入时的字符集问题
现象:SQL 导入成功,但页面上中文全部显示为乱码,患者姓名、科室名称全是问号。
原因:SQL 文件本身是 UTF-8 编码,但 MySQL 客户端的连接字符集不是 UTF-8,导致导入时中文在传输过程被转错。另一种情况是本机数据库默认字符集是 latin1,表创建出来就是 latin1 字符集。
解决:导入前先执行SET NAMES utf8mb4;,确保客户端连接用 UTF-8 传输。同时把数据库和表字符集统一改成 utf8mb4:
ALTER DATABASE clinic_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE appointment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;被乱码影响的主要是表里的中文初始数据,比如科室名称、药品名称、管理员账号的显示名。数据已经变成乱码的记录需要清掉重新导入,因为乱码意味着字节已经损坏,改字符集修不回来。我把这套操作倒腾完花了半小时,后来养成了一个习惯:导入 SQL 之前先看一眼文件头部的建库语句,确认里面有没有显式指定字符集。
5.5 Node 版本太新导致前端编译失败
现象:npm install 能跑完,但npm run serve启动时直接报Error: error:0308010C:digital envelope routines::unsupported。
原因:Node 17 及以上版本用了新版 OpenSSL,而 vue-cli 3 或 4 依赖的 webpack 4 还在用旧版加密算法。这个报错前两年网上讨论很多,属于典型的环境不适配问题。
解决:方案一最省事,把 Node 换成 16 或 14 重新安装依赖。方案二如果不想换版本,可以在启动前加一行参数,绕过新版 OpenSSL 的检查。
export NODE_OPTIONS=--openssl-legacy-provider npm run serve这是 Linux 和 macOS 的写法。Windows 下用set NODE_OPTIONS=--openssl-legacy-provider。这种方法能应付大多数人,但如果你做了 vue-cli 向 vite 迁移的计划,不如直接用 Node 16 省心。这个项目的前端依赖锁定在 vue-cli 时代,按最保守的版本配环境能少踩一半坑。
6. 给推荐加一层内存缓存:把热数据放进 Map 的改写思路
协同过滤算法有个特点:相似度矩阵计算一次之后,在数据不发生变化时结果是一样的。这个项目每次打开推荐接口都实时重新算一遍,数据量小的时候没问题,但答辩演示时反复刷新页面,每次都走一遍矩阵计算,响应时间看着不舒服。我拿到项目后做了一次小改造,把医生相似度矩阵和 TopN 推荐结果放进内存缓存,效果非常明显,推荐接口响应时间从几百毫秒降到几十毫秒。
改造思路很简单:用一个ConcurrentHashMap存放医生相似度矩阵,用另一个ConcurrentHashMap存放每个患者的推荐结果,加上过期时间。推荐逻辑执行前先去缓存里拿,缓存没有才触发计算。代码写在 recommender 包里新增一个 CacheService,不侵入原有逻辑。
我用的是最简单的定时失效方案,在缓存 map 里同时存时间戳,每次读取时判断是否超过 10 分钟。这个时间对诊所场景足够,因为新的预约行为不会每秒钟都产生,10 分钟内的推荐结果不会明显过期。注意如果演示时要现场展示新预约影响推荐结果,需要加一个手动清缓存的方法,在预约成功接口里调用一下。
从那以后,我每次处理这种带算法但数据量又不大的一体化项目,都强制先跑一遍「算得慢是不是因为没缓存」这个排查动作,别急着上 Redis,先用 Java 原生的 Map 撑住,业务量真上去了再迁移到独立缓存组件。希望帮到你。
本文还有配套的精品资源,点击获取