社区医院管理系统这种项目,在各类源码分享站上其实很常见,但“能跑”和“适合用来学、用来改、用来上线试点”是两回事。我拿到这套 SpringBoot + Vue + MySQL 的源码后,第一反应是先把它跑起来看目录结构,然后逐模块整理业务闭环。你要说它是多庞大的工程,谈不上,但它把门诊日常里最关键的几个环节——患者建档、挂号、医生看诊、开药收费——全部串起来了,对于一个面向社区医疗场景的信息化项目来说,这个体量和复杂度恰到好处。
这篇文章我会按自己的实操顺序来写:先拆项目思路和技术选型,再分别讲后端、数据库、前端的关键实现,最后给出可直接照抄的启动步骤和踩坑记录。无论你拿这套源码做什么用途——毕业设计、练手项目、小诊所的信息化改造参考,跟着走一遍,基本能把整个前后端分离项目的运行脉络摸透。
1. 项目全貌:从标题里拆出这系统真正要做的事
1.1 社区医院与综合医院的信息化差异
一说到医院管理系统,很多人脑子里浮现的是三甲医院那套庞大的 HIS(Hospital Information System),预约挂号、电子病历、影像传输、检验报告、医保接口,十几个子系统互相调用,光部署文档就上百页。但社区医院、街道卫生服务中心、校医院、小型民营诊所,它们的业务要轻得多。
社区医院的日门诊量通常在一百到三百人次之间,科室集中在全科、内科、儿科、中医科、康复科。它需要的不是大而全的 HIS,而是一套能把“来病人了、挂了号、看了诊、开了药、收了费、记了账”这件事跑顺的小系统。这套源码的定位正是后者。从模块划分就能看出设计者心里有数:患者管理、医生管理、挂号管理、药品管理、收费管理,再加上一个简单的统计看板。没有影像、没有病历文书引擎、没有医保接口,做的是基础台账。
知道这个定位很重要。你看代码时就不会拿“三甲标准”去苛责它哪里不完善,而是会理解:每个表为什么这么设计,每个接口为什么只暴露这几个字段。它是为轻量化场景服务的,小而完整是它的特点。
1.2 系统的核心功能闭环
跑通项目后,我梳理出来的业务主线是这样的:管理员在后台维护医生和药品数据,患者在挂号窗口建档,挂号员选择科室和医生完成挂号,医生在诊疗界面查看挂号记录并开出药品处方,收费员确认收费并生成收费记录。整个流程是一条直线,每个环节的状态都是可追踪的。
挂号表里有 status 字段(已挂号、已就诊、已取消、已退号),药品表里有库存字段和预警阈值,收费记录表和挂号记录表通过 patient_id 和 order_id 关联。这些设计单独看不稀奇,但它们凑到一起,恰好把一个门诊流程的闭环完成了。对于学习 SpringBoot 和 Vue 的开发者来说,这种“一个角色对应一组页面、一组接口”的结构,是最容易看懂也最容易二次开发的。
如果你是拿它应付毕业设计,我建议把“业务闭环”写进论文的可行性分析里,这一条比堆砌十个功能模块更有说服力,因为评审老师看重的就是“这系统能不能真正用起来”。
1.3 为什么是 SpringBoot + Vue + MySQL 这套组合
技术选型没有悬念,但值得说说背后的逻辑。社区医院管理系统这种体量的项目,最忌讳的是引入太重的东西。不需要微服务拆分,不需要消息队列缓冲,不需要引入 Redis 做缓存,更不用上什么工作流引擎。SpringBoot 负责提供接口和事务管理,Vue 负责页面交互,MySQL 负责持久化,这套组合是当前国内中小型管理系统最成熟的搭配,没有之一。
SpringBoot 的意义在于它把 Web 开发里最繁琐的配置自动化了。以前用 SSH 那套,光是配置事务和连接池就得折腾半天,SpringBoot 一个注解全搞定。Vue 这边呢,组件化开发让页面复用变得非常自然,挂号页面、患者列表、药品管理,每个页面独立维护,改起来不会牵一发动全身。MySQL 则是关系型数据库里最适合中小体量的选择,免费、稳定、招人好招,社区医院的信息科也能接得住。
有一点要提醒:这套项目只要换掉几个连接参数,再处理一下跨域问题,就能直接部署到云服务器上。这点在后面的“直接运行”部分我会详细讲。
2. SpringBoot 后端:目录结构、配置与登录鉴权
2.1 后端三层架构与目录组织
源码后端的包结构是标准的 controller、service、mapper、entity(或者叫 pojo/model,不同版本叫法略有差异)四层。我先给你看一遍我理解的职责划分,这对后续改代码很有帮助:
- controller:接收前端请求,做了参数基础校验后调用 service,把结果封装成统一返回体。
- service:真正的业务逻辑层,比如挂号时检查号源是否充足、收费时检查药品库存是否扣减成功。
- mapper:数据访问层,负责拼接 SQL 或使用 MyBatis-Plus 的封装方法完成数据库操作。常见的是直接继承 BaseMapper。
- entity:和数据库表字段一一对应的实体类,这里通常会配合 Lombok 用 @Data 注解减少 getter/setter 代码。
用这套源码学习时,有个很好的练习方式:自己顺着“患者查询”这个功能,从 controller 入口一路追到 mapper 的 SQL,把一条请求的完整链路画出来。画完三个这样的链路,你对 SpringBoot 项目的理解基本就过关了。
有些裁剪过的源码项目会直接在 controller 里写业务逻辑,把 mapper 揉进 controller。这套源码我没看到这种情况,业务代码还是乖乖待在了 service 里。这一点要给设计者点个赞,因为这种结构让你加日志、加事务、做单元测试都方便得多。
2.2 application.yml 里的关键配置项
整个后端配置几乎都集中在 resources/application.yml 里。我直接把最核心的部分拆出来说明,你照着改就能连上自己的环境:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/community_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl- url 里的 serverTimezone=Asia/Shanghai是必须的。如果不加,MySQL 驱动在解析日期时间字段时会直接报时区错误,报错信息很吓人,但根因就这一个。
- driver-class-name 用了 com.mysql.cj.jdbc.Driver,这是 MySQL 8.x 的驱动类。如果你本机用的是 MySQL 5.7,需要改成 com.mysql.jdbc.Driver 吗?建议直接用 8.x 驱动并兼容 5.7 数据库,因为新版驱动向后兼容,省得绕弯路。
- map-underscore-to-camel-case开启后,数据库字段如 create_time 才能自动映射到实体类里的 createTime,否则你要在每个字段上加 @TableField 注解,太麻烦。
- log-impl 配置成 StdOutImpl后,控制台会打印每一条 SQL 语句。调试联调阶段千万别关它,你能直观地看到 MyBatis 实际执行的 SQL 是什么样的。
还有个小细节:如果你从别处拿到的源码里没有密码加密配置,而你又想把数据库密码脱敏,可以了解一下 jasypt 这个库,但前期折腾它意义不大。先让项目跑起来,安全加固是后话。
2.3 登录鉴权的实现方式:拦截器 + JWT
轻量管理系统的登录鉴权是一个绕不开的话题。这套源码没有引入 Spring Security,我看了之后觉得这反而更合适。Spring Security 功能强大,但它有一套完整的过滤器链和对象模型,新手第一次配置,光明白 SecurityContextHolder 是什么就要半天,更别提动态权限配置了。
流程非常经典:用户提交用户名密码到 /login 接口,后端核对数据库里的用户记录,生成一个 JWT token 返回给前端;前端把 token 存起来(常见的是 localStorage 或 vuex/pinia);之后的每次请求,前端都在请求头里带上 Authorization: Bearer token;后端写一个拦截器,在请求进入 controller 之前校验 token 是否有效,无效则返回 401。
核心代码一般就这几个类:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } // 从请求头中获取token String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && JwtUtil.verifyToken(token)) { return true; } response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"登录状态已失效,请重新登录\"}"); return false; } }然后在 WebMvcConfigurer 里注册拦截器。你只需要保证 JwtUtil 里生成和解析 token 用的密钥一致,签发的有效期合理(一般 2 到 24 小时)就够了。
这里有个阅读源码时要留意的坑:有些版本会把 token 直接放在请求参数里而不是请求头里。如果你遇到前端想尽办法带 token 但是后端拿不到的情况,先检查一下拦截器到底从哪个位置取 token。这套源码用的是请求头方式,属于主流做法。
2.4 角色权限与接口分层的处理
常见的管理系统会把用户角色拆成管理员、医生、收费员。这套源码没有做细粒度的按钮级权限控制,登录后的菜单显示是前端根据角色字段判断的。后端接口层面,除少数敏感操作外,基本依赖登录校验兜底。
从学习角度,这种粗粒度方案反而适合入门。你不需要掌握复杂的权限模型,只需要理解“后端按接口逻辑校验、前端按角色渲染页面”的分工。真要上线使用,这套权限粒度对社区医院是够用的,因为内部人员本来就不多,角色就那么几个。
如果你以后要扩展,我给你指个方向:把用户角色表拆成 user、role、menu 三张表,后端增加一个基于注解 @RequireRole 的拦截器,遇到注解就比对当前用户角色。原理不复杂,一旦把这层补上,你的项目从毕业设计角度就有话可写了。
3. MySQL 数据库设计:从建表脚本反推业务模型
3.1 核心表的字段设计与关联关系
我把数据库脚本导入后,逐个表看了一遍。这套系统的表设计延续了“轻量但完整”的风格。这里挑几张关键表说说字段设计的门道。
用户表(sys_user),主要字段包括:id、username、password、real_name、role、status。这里有个重点,密码字段不是明文存储的,而是经过加密处理过的字符串。如果你拿来学习,以后自己写注册功能时务必沿用这种加密方式,不要直接存明文,哪怕项目暂时不上线也要坚持这个习惯。status 字段用来表示账号是否可用,比直接物理删除数据要安全得多,也便于保留操作记录。
患者表(patient),包含姓名、性别、出生日期、身份证号、手机号、家庭住址。身份证号一般会加唯一索引,因为一个真实患者在社区医院建档,理论上不应该存在两条档案。不过如果患者卡号(patient_no)是独立生成的,你就明白了,它才是业务侧真正的关联键。
医生表就更有意思了。医生的基础信息往往复用用户表,但排班信息、所属科室、职称信息会放在单独表里。这种“人员基础信息与业务信息分离”的做法,在数据库设计里非常典型。你看源码时可以把注意力放在:医生表的 doctor_id 是怎么和 sys_user 的 user_id 对应起来的。
科室表是独立的一张表,字段不过 id、dept_name、description 之类,但它是整个系统的根基,因为医生要挂在科室下,挂号列表要先按科室筛选。很多新手做设计时容易把科室写成医生表里的一个字符串字段,前期是省事了,后面想统计各科室的门诊量就傻眼了。
3.2 挂号、处方与收费:业务表怎么串成流程
挂号表(registration)是这套系统的核心。字段包括:patient_id、doctor_id、registration_time、visit_date、visit_time(上午/下午)、status、transaction_amount。最关键的查询场景是“某个医生某一天有多少人挂号”“某患者在该院的历史就诊记录”,所以表设计上 patient_id 和 doctor_id 必须建索引,不然数据量一上来,联查就会很慢。
药品表(drug)除了常规的名称、规格、厂家、零售价,还配了库存字段 stock。这里我建议看代码时特别关注:收费的时候,是“先扣库存再写收费单”,还是“写收费单的时候同步扣库存”,业务顺序直接影响到事务的写法。
处方和收费环节,因为药品可能有多种,一般会拆成主表和明细表。主表记录本次处方/收费的患者、总金额、操作人、时间;明细表记录每一种药的名称、单价、数量、小计。这套源码在这一点上处理得是对的。我在写这种需求的时候总对新手说一句:只要出现“一单包含多个子项”的场景,主表和明细表拆开就是标准答案。
3.3 金额字段为什么必须用 decimal
这是我对做管理系统的朋友反复强调的一类经验:涉及金额的字段,一律使用 decimal,不要用 float 或者 double。浮点数在计算机底层是二进制表示的,它在做小数运算时有精度损失。举个最直观的例子:0.1 + 0.2 在浮点运算里并不精确等于 0.3,否则医院收费明细统计出来的总和很容易出现几分钱的误差,财务不管你是计算机原理导致的还是算错了,直接跟你翻脸。
用 decimal(10,2) 定义金额,数据库层面就会按定点方式存储和计算,记账就准确了。这套源码里挂号费、药品单价、收费金额都用了 decimal(10,2),这是正确示范。你如果发现手头别的源码里金额字段是 double,建议第一时间改掉。
3.4 导入脚本时要注意编码和版本
源码库一般会带一个 .sql 文件,里面包含建表语句和初始数据。我用 Navicat 导入时第一次没注意,直接双击打开,结果表注释全变成了问号。原因很简单:脚本文件是 UTF-8 编码,而客户端连接默认用了其它字符集。正确的导入姿势是用命令行或者在工具里指定 utf8mb4 连接:
mysql -u root -p --default-character-set=utf8mb4 community_hospital < init.sql然后确认三件事:数据库字符集是 utf8mb4、排序规则是 utf8mb4_general_ci 或者 utf8mb4_unicode_ci、表的引擎是 InnoDB。字符集直接决定你在页面输入生僻字、输入外文符号时会不会乱码;引擎则决定你的业务操作能否使用事务。MySQL 8.0 默认配置基本没问题,如果你是 5.7 且有历史包袱,检查一下即可。
4. Vue 前端:从项目创建到页面联调
4.1 Vue 前端环境与项目结构
前端代码用的 Vue 2 还是 Vue 3,要从 package.json 里看。如果用 Vue 3,配合的是 vue-router 4 和 Pinia(状态管理);如果还是 Vue 2,多半是 vue-router 3 和 Vuex。这个版本差异直接影响你运行命令时的依赖安装。
我建议运行前先把 Node.js 版本和环境说清楚。Node 版本太新或太旧,装依赖时都会吃瘪。经验法则:Vue 3 项目用 Node 16 或 18 LTS 版本基本畅通;Vue 2 老项目如果依赖里有 node-sass,那么 Node 版本要更低,否则 node-sass 编译直接让你体验什么叫“装了一晚上依赖全白费”。
前端的标准目录大致是:views 放页面组件,router 放路由表,api 放 axios 请求封装,store(或 pinia 目录)放全局状态。我拿到项目时先打开 router/index.js,从上到下扫一眼有哪些路由,整个系统有几个页面立刻心里有数。这是一个很高效的逆向理解项目的方式。
4.2 登录、路由守卫与主页框架
前端登录成功后一般会把 token 和用户信息都存起来,然后跳转到首页。关键的防御点在于路由守卫:如果用户没有登录,就访问首页之外的页面,应该被重定向回 /login。这个逻辑一般在 router/index.js 里这样写:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })从这里能看出前后端分离项目的一个习惯:前端控制“能不能看到页面”,后端控制“能不能调通接口”。二者缺一不可。如果你只依赖前端守卫,别人直接调你的接口就能绕过权限;如果只依赖后端拦截器,体验会很差,用户明知道没登录也不给他跳转登录页的反馈。
4.3 axios 封装与跨域配置
axios 封装这块,核心两件事:统一请求前缀、统一在请求头里挂 token。拦截器几乎是每个项目必写的,我摘一段通用配置供参考:
import axios from 'axios' import { Message } from 'element-ui' // 或 element-plus const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } Message.error(error.response?.data?.msg || '网络异常,请重试') return Promise.reject(error) } )这里 baseURL 用了 /api 而不是后端完整地址,是为了配合开发环境的代理。如果你直接写成 http://localhost:8080,就会立即遭遇跨域问题,浏览器拦截请求,页面白屏或者报错一大片。要解决跨域,简单粗暴的方法是在后端加 CORS 配置;更推荐的是在 vue.config.js 里配置代理:
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } })这样前端发出的 /api/login 请求就会被 devServer 代理转发到后端的 /login,既绕过了跨域问题,又保持了代码里的请求路径整洁。这套源码里如果默认开发端口是 8080,而后端 Tomcat 也默认 8080,那你就得改其中一个端口,通常把前端的 devServer.port 改成 3000 或 5173 比较省事。
5. 让项目“直接运行”:从零开始跑通的完整清单
5.1 MySQL 准备:推荐版本与导入 SQL 的正确姿势
标题里说“可直接运行”,我实操后确认它确实可以直接运行,需要的前提是 MySQL 环境已就绪。我先说版本建议:MySQL 8.0 任意小版本均可,5.7 也可以跑,但如果你是从零安装,直接上 8.0 就行了,性能和默认字符集的体验都更好。
装好后,启动服务,用客户端工具创建数据库:
CREATE DATABASE IF NOT EXISTS community_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后导入项目里的 init.sql。这一步不管用 Navicat 还是命令行都行,核心就一条:确认导入过程没有报错,导入完成后,打开表列表应该能看到 sys_user、patient、doctor、registration、drug、charge 等几张核心表。如果缺表,多半是 SQL 中断了,重新导入,必要时先删除数据库再导入一次。
5.2 后端启动步骤:跳过三个最容易出问题的细节
后端启动我用的是 IDEA 配合 Maven。导入项目后,第一步是等 Maven 把依赖拉完。这一步经常卡住,尤其是国内网络环境下,扛不住就去配置阿里云 Maven 镜像。等右下角进度条消失、依赖列表没有红色报错,才能继续下一步。
启动前改三个地方:
- 确认 application.yml 里的数据库名、用户名、密码与你本机 MySQL 一致。
- 确认 MySQL 服务正在运行,如果连接不上,先用命令行
mysql -u root -p验证账号密码是否正确。 - 确认 8080 端口没被占用。会被占用的典型原因包括:自己之前起过别的 SpringBoot、本地有 docker 端口映射、某些软件占用了 8080。
改完后直接运行主类中带 @SpringBootApplication 的启动类。看到下面的日志就说明后端起来了:
Tomcat started on port(s): 8080 (http) Started CommunityHospitalApplication in 3.76 seconds有一种情况很常见:启动过程没报错,但一调用接口就 500,看了日志定位到是空指针。这种多半是数据库里缺数据,比如登录查询用户时查到 null 后没处理。遇到这种问题,别急着怀疑源码,先看看是不是初始化数据没导完整。
5.3 前端启动步骤:依赖安装命令与版本适配
前端我建议用 Visual Studio Code 打开,终端执行:
npm install这一步失败率不低,原因大多出在依赖版本和 Node 版本不匹配。如果你装的是老版本项目而他锁定了 node-sass,npm install 过程中经常会在编译 node-sass 时挂掉。快速解法是切换 Node 版本,比如用 nvm 切到项目要求的版本(项目 README 里一般会写)。如果问题依旧,就把 node_modules 彻底删掉,重新npm cache clean --force后再装一遍。
依赖装完后:
npm run serveVue CLI 项目默认启动在 8080,如果端口冲突,Vue 会提示你换个端口;Vite 项目则默认 5173。看到App running at: Local: http://localhost:3000这样的信息,浏览器访问对应地址,登录页能显示出来,整个系统就算端到端跑通了。
5.4 从登录页开始验证整个链路
在登录页用系统内置的管理员账号登录(账号密码一般在 README 或 SQL 初始化脚本里)。登录成功后,页面跳转首页,左侧菜单应该显示:首页、患者管理、医生管理、挂号管理、药品管理、收费管理等模块。分别点开,看数据能否正常加载。
如果登录正常,但列表页转圈后无数据,打开浏览器开发者工具的 Network 面板,看请求状态码。401 是 token 失效,404 是接口路径不对,500 是后端报错。把第一只拦路虎定位到具体层以后,问题就解决一半了。
6. 常见问题排查与个人经验记录
6.1 启动后端时报时区错误
报错信息大致是这个样子:“The server time zone value ' CST' is unrecognized or represents more than one time zone”。我第一次遇到时还以为是系统时间不对,折腾了半小时才发现是连接串的问题。处理方式就是在 datasource.url 中加上 serverTimezone=Asia/Shanghai。这是最常见、最好解决但也最容易让人误判的问题。
6.2 MyBatis-Plus 版本过高导致启动失败
这套源码如果用的 MyBatis-Plus 版本较新,而项目引用的 SpringBoot 版本较旧,有概率出现方法签名冲突。我遇到过类似情况,处理方法很无脑但有效:把 mybatis-plus-boot-starter 版本降到和 SpringBoot 主版本相近的版本。比如 SpringBoot 2.3.x 搭配 MyBatis-Plus 3.4.x 是稳的。升级或降级后别忘记mvn clean一下,把旧的 target 清掉再重新编译。
6.3 前端页面加载但接口 404
这种情况表面上是“前端没连上后端”,其实是路径对不上。常见原因是 axios 的 baseURL 和后端 controller 的 RequestMapping 前缀不一致。你把后端接口的完整路径在浏览器里直接访问一遍,如果返回 JSON 数据,说明后端没问题;如果前端代理配置的 /api 转发没生效,返回的就是前端 devServer 的 404 页面。对号入座,专心修一处即可,不要全盘重来。
6.4 数据库连接乱码与中文字段显示问题
页面注册的患者姓名、地址乱码,优先查数据库连接的 characterEncoding 是否等于 utf8,以及数据库表本身的字符集。把数据库字符集统一改成 utf8mb4,重开应用,清除浏览器缓存刷新一遍,一般就能解决。注意有些时候改完配置后需要重启后端而不是前端。
6.5 我在这套源码上实际改动后的体会
把这套项目完整跑起来后,我做了一个小改造:给药品列表增加了按库存量升序排序的功能,同时在前端表格顶部加了库存预警的统计卡片。整个改动涉及后端一个 mapper 方法、一个 service 方法、几个 Controller 接口和一个前端页面组件,总共用时不到半小时。这正是这类源码最大的价值——它把全栈开发里最常用的增删改查和联调路径完整地演示了一遍,你可以在它基础上快速加自己的业务场景,而不需要从零搭架子。
最后分享一个我跑这种“可直接运行”项目时的习惯:不要一上来就埋头看代码。先让数据库、后端、前端都跑起来,登录进去把每个页面点一遍,对系统长什么样有个整体印象,再回头读代码。读懂一个系统的最高效路径永远是“先跑通、再拆解、最后动手改”,这也是我推荐每个拿这套源码学习的人采用的顺序。