最近在整理手上的一套牙科就诊管理系统源码,技术栈是SpringBoot后端+Vue前端+MySQL数据库。这是一套典型的全栈项目,前后端代码分离,初看目录结构可能会让人有点懵,但只要把数据流捋顺,这套系统的业务逻辑其实非常清晰,而且很多模块可以直接改造复用。这篇文章打算把整个系统从业务设计到本地运行的所有关键细节拆开讲一遍,既给你看功能模块、表结构、接口设计和前端页面的核心逻辑,也把我在跑这套工程时踩过的坑一并交代清楚。不管你是毕业设计找参考项目、诊所想做信息化,还是单纯想学SpringBoot+Vue前后端分离的完整写法,这篇都值得看完。
1. 为什么是牙科:系统业务价值与功能边界
1.1 牙科诊所与综合医院的信息化管理差异
很多人一听到"医院信息系统"就想到HIS系统那一套庞然大物,但实际上牙科诊所的信息化管理需求和综合医院差别很大。综合医院要管住院、管大型设备、管检验检查报告流转,而牙科诊所的核心业务是门诊诊疗,业务链条更短但重复性很高。
牙科的特点是复诊率高。补牙要分次做,根管治疗要去三四趟,正畸更是按月复诊持续一两年。这就意味着系统必须把"患者历次就诊记录"作为核心资产来管理,而不是像综合医院那样以"一次挂号一次诊疗"为基本单元。我拿到的这套源码,在设计上就是把患者档案放在最前面,诊疗记录、收费记录、预约记录全部挂在患者ID下面,这种模型和牙科业务是匹配的。
另外一个牙科特有的痛点是"牙位"。"右上第一磨牙""左下第二前磨牙"这种描述,纸质病历上写起来含糊,不同医生记录习惯还不一样。这套系统在检查记录模块里专门设计了牙位选择器,可以按国际牙位编号标准逐颗标记患牙,这是通用诊所系统很少做精细的细节。系统上线后医生输入病历的时间能明显减少,前台也能在患者来电时快速调出历史诊疗信息。
1.2 系统功能模块总览:从预约到报表的闭环
这套系统整体覆盖了一条完整的门诊业务链路:患者建档、在线排队、医生接诊、检查记录、治疗计划、收费结算、药品材料出库、经营统计。
具体模块我梳理下来有这么几块:
- 患者管理:建档、编辑、查询,支持按姓名、手机号、病历号检索。患者列表里有历次就诊次数和累计消费金额,方便前台做客户维护。
- 预约挂号:按医生排班、按日期时段展示号源,支持初诊和复诊预约,复诊预约可以关联上次就诊记录。
- 医生工作站:待诊队列、患者基本信息展示、牙位图、病历录入、治疗方案录入、医嘱开立。
- 收费管理:收费项目维护、划价收费、退费、收费记录查询,支持现金和扫码收款登记。
- 药品材料管理:药品材料入库、库存查询、出库记录,收费时联动扣减库存。
- 系统管理:用户管理、角色权限、菜单管理、系统参数配置。
- 统计报表:日营收报表、医生工作量统计、患者来源统计、高频诊断统计。
功能谈不上包罗万象,但门诊日常运营的闭环是完整的。对一个中小型口腔诊所来说,这套系统上线后基本可以替代纸质台账加Excel表格的管理方式。
2. 技术选型与项目结构:一套可直接运行方案背后的取舍
2.1 为什么这套技术栈适合中小型项目
SpringBoot+Vue+MySQL这套组合,放到2025年看可能不算新潮,但它恰恰是这种中小型管理系统最稳妥的选择。选型这件事,我一直认为要看你项目的真实约束条件,而不是追最新的框架。
SpringBoot在后端领域的地位不用多说。它对内嵌Tomcat、自动配置、起步依赖这些机制的支持,让开发人员不用去折腾繁琐的XML配置,一个main方法就能把服务跑起来。对于牙科门诊这类业务,接口数量通常在几十到一百多个,SpringBoot的Controller-Service-Mapper三层结构足够清晰,维护成本低,招人也容易。
Vue作为前端框架,学习曲线平滑,组件化开发适合管理系统这种大量表单、表格、弹窗交互的场景。配合Element UI组件库,后台管理界面的开发效率很高。MySQL就更不用提了,开源免费,生态成熟,对几十张表、百万级以下数据量的系统来说性能绰绰有余。
拿这套系统来说,后续如果要部署到诊所的本地服务器或者云服务器,这套技术栈的部署成本也很低。JDK加Nginx加MySQL三个环境搞定,内存2GB的小机器就能很流畅地跑起来。换成微服务架构反而是自找麻烦。
2.2 后端工程结构与前端工程结构
这套源码的前后端是分离的目录结构,我先把工程骨架梳理出来。
后端SpringBoot部分的包结构如下:
src/main/java/com/dental/clinic ├── common # 通用模块:统一返回结果、全局异常处理、工具类 ├── config # 配置类:跨域配置、拦截器配置、MyBatis配置 ├── controller # 接口层:接收请求、参数校验、返回结果 ├── service # 业务层:核心业务逻辑 │ └── impl # 业务实现类 ├── mapper # 数据访问层:MyBatis接口 ├── entity # 数据库实体类 ├── dto # 数据传输对象:接收前端参数 ├── vo # 视图对象:返回前端数据 └── security # 登录鉴权相关:JWT工具、拦截器、注解前端Vue部分的结构如下:
src ├── api # 接口请求封装,按模块拆分 ├── assets # 静态资源 ├── components # 公共组件 ├── layout # 整体布局框架 ├── router # 路由配置 ├── store # Vuex状态管理 ├── utils # 工具函数,包括request.js请求封装 └── views # 页面组件 ├── patient # 患者管理页面 ├── appointment # 预约挂号页面 ├── doctor # 医生工作站 ├── charge # 收费管理 ├── drug # 药品管理 ├── statistics # 统计报表 └── system # 系统管理前端是标准的管理后台结构,views目录下每个业务模块一个文件夹,页面的入口和组件分开,维护起来很直观。api目录下的文件和后端controller基本一一对应,找接口的时候按模块搜就行。
2.3 数据库表设计的关键实体
数据库是这套系统的核心资产,表设计直接决定业务能不能顺畅跑起来。我统计了一下,总共三十多张表,挑几张核心的说说设计思路。
患者表(patient)是整个系统的数据枢纽,核心字段包括姓名、性别、出生日期、手机号、身份证号、过敏史、既往病史等。其中手机号设置了唯一索引,每次复诊患者报手机号就能定位档案。身份证号可以选填,因为部分牙科诊所的患者是未成年人,没有身份证也很正常。
预约表(appointment)包含诊室ID、医生ID、患者ID、预约日期、开始时间、结束时间、预约类型、状态字段。状态字段用来区分已预约、已到诊、已完成、已取消。这里有个设计细节:时间段不是固定写死的,而是根据医生排班的时段模板动态生成,这样不同医生可以设置不同的接诊时长。
诊疗记录表(medical_record)是医生工作站的核心表,字段涵盖主诉、现病史、检查所见、诊断结果、治疗方案、医嘱、牙位信息。牙位信息在MySQL里存的是字符串,用逗号分隔的编号列表,比如ROOT_CANAL字段里存"16,17,26"这样的值,表示右下第一磨牙、右下第二磨牙、左上第一磨牙。这种设计虽然不算高度范式化,但胜在简单直接,查询的时候用LIKE或者FIND_IN_SET就能处理,符合中小型系统的定位。
收费记录表(charge_record)关联了患者ID、收费项目ID、数量、单价、金额、操作人等字段。为了记录收费快照,收费项目的名称和价格直接冗余在表里,而不是通过关联去查。这样一旦后续调整了收费项目的价格,历史收费记录不会跟着变,财务数据更加可靠。
3. 后端SpringBoot核心实现:从登录鉴权到业务接口
3.1 统一响应体与全局异常处理
看一个SpringBoot项目写得好不好,先看它的响应体封装和异常处理,这几乎是所有管理系统的地基。这套系统在common模块里定义了一个统一的响应类,结构是Code、Msg、Data三件套。
@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); return result; } }为什么要统一响应体?因为前端Axios拦截器只需要判断code字段就能知道请求成功还是失败,不用对每个接口单独做判断。比如前端request.js里只要写一句if (response.data.code !== 200) { Message.error(response.data.msg); return Promise.reject(error); },所有业务异常就能统一提示。
全局异常处理用的是@RestControllerAdvice加@ExceptionHandler的组合。项目里自定义了一个业务异常类BusinessException,Service层遇到业务不满足的情况直接throw new BusinessException("库存不足"),异常处理器捕获后返回对应的错误码和消息。这样Controller层的代码就非常干净,不必到处都是try-catch。
3.2 JWT登录与角色权限控制
登录鉴权用的是JWT方案,核心流程是:用户登录成功后,后端生成一个签名字符串返回给前端,前端存到localStorage或Vuex里,之后每次请求在Header里带上Authorization: Bearer <token>,后端拦截器校验签名和过期时间。
这套系统在后端定义了一个@RequirePermission注解,标注在Controller方法上,配合拦截器实现接口级别的权限控制。
@RequirePermission("patient:add") @PostMapping("/patient") public Result<Long> addPatient(@RequestBody PatientSaveDTO dto) { return Result.success(patientService.save(dto)); }权限拦截器里的核心逻辑,简单概括就是:取出token,解析出用户信息和角色编码,从Redis或内存缓存中查出该角色拥有的权限编码集合,然后判断当前接口标注的权限编码是否在集合中。如果没有权限,直接抛出403异常。
在实际项目中,权限粒度做到按钮级别是合理的。比如"患者管理"页面里新增按钮要控制权限,删除按钮也要控制权限。这套系统的前端路由守卫会和后端权限配合使用——有权限的用户才能看到对应菜单,但即使有人绕过前端直接请求接口,后端的权限拦截器也能挡住,双重保险。
3.3 牙位诊断与收费业务场景的接口设计
这套系统里我觉得比较有业务特色的接口是牙位诊断相关的那几个。前端渲染一张标准的牙位图,用户点击牙齿编号后,后端接受的数据格式是这样一个JSON结构:
{ "patientId": 1001, "teeth": [ {"toothNo": "16", "diagnosis": "深龋", "treatment": "充填治疗"}, {"toothNo": "17", "diagnosis": "牙髓炎", "treatment": "根管治疗"} ], "mainComplaint": "右下后牙冷热刺激痛一周" }后端Service层拿到这个结构后,遍历teeth数组,把每颗牙的诊断和治疗方案拆成子记录存入诊疗明细表。这里用的就是典型的MyBatis批量插入,用<foreach>标签组装insert语句,一次请求插入多条明细,性能比循环单条插入好得多。
收费模块的接口设计同样值得参考。诊所划价的场景是医生在诊疗计划里勾选多个治疗项目,点击"划价"后,后端自动汇总所有项目金额、扣减对应库存、生成收费记录。这个接口涉及到事务管理,加@Transactional注解保证要么全部成功要么全部失败。我专门测试过库存不足时的场景:划价接口返回库存不足的异常信息,收费记录和库存扣减都不会落库,事务回滚是可靠的。
4. 前端Vue页面核心交互逻辑
4.1 路由与权限守卫设计
前端路由配置放在router目录下,采用动态路由和权限控制相结合的方式。系统初始化时,前端通过用户信息接口获取该用户拥有的菜单权限,然后动态添加路由,而不是一次性把所有路由都注册进去。这样做的好处是用户没有权限的模块,连代码都不会加载,体验上更干净。
具体实现依赖Vue Router的addRoute方法。前置守卫里做两件事:一是判断token是否存在,如果未登录则重定向到登录页;二是判断用户信息是否已加载,如果没有则调接口拉取用户信息和权限菜单,然后next({ ...to, replace: true })重新进入当前路由,确保路由已注册完成。
这套系统在权限上设计了三种角色:超级管理员、医生、前台。超级管理员可以看到系统管理菜单,医生只能看到医生工作站和患者查询,前台看到的是预约、挂号、收费这些菜单。路由表按角色过滤的代码很直观:
const dynamicRoutes = filterRoutes(asyncRoutes, userInfo.permissions) function filterRoutes(routes, permissions) { const res = [] routes.forEach(route => { const tmp = { ...route } if (hasPermission(permissions, tmp.meta.permission)) { if (tmp.children) { tmp.children = filterRoutes(tmp.children, permissions) } res.push(tmp) } }) return res }4.2 预约日历与诊室看板的交互实现
预约页面是这套系统交互最频繁的页面。诊所前台的日常操作就是看着日历选择日期、选择医生、选择时段、录入患者信息,一气呵成。
前端预约页面用Element UI的日历组件做基础,当日历日期切换时触发change事件,向后端请求该日期下所有医生的排班和号源占用数据。返回的数据结构包含每个医生的时段列表,每个时段有开始时间、结束时间、剩余号源数。前端渲染时把剩余号源为0的时段置灰,点击后弹出患者选择对话框。
医生工作站的待诊看板做的是轮询刷新。因为牙科诊所的接诊流程是前台叫号、医生接诊,需要实时看到最新队列状态,所以前端用setInterval每30秒调一次待诊队列接口,有变化时刷新列表并播放提示音。当然,如果要做成真正生产级,这个场景用WebSocket更合理,但考虑到源码的定位是中小型诊所,轮询方案已经够用,而且实现和维护都更简单。
4.3 Axios封装与Token管理
前端的请求全部走utils/request.js这个统一入口,基于Axios做了二次封装。核心逻辑包括:请求拦截器里从localStorage取token并放入请求头,响应拦截器里统一处理code码和HTTP状态码。
service.interceptors.request.use(config => { const token = getToken() if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } if (res.code === 401) { // token过期,清除登录态,跳转登录页 removeToken() router.push('/login') return Promise.reject(new Error('登录已过期')) } Message.error(res.msg || '系统错误') return Promise.reject(new Error(res.msg || '系统错误')) }, error => { Message.error(error.message || '网络错误') return Promise.reject(error) } )这样每个业务页面调接口时,只需要关心成功返回的数据,异常情况都被拦截器消化掉了。我在用这套源码的时候注意到,它的接口调用函数统一从api目录导出,每个页面的接口归属清晰,比如patient.js里就放患者模块的所有接口定义,这种组织方式在多模块开发时非常实用。
5. 环境准备与本地运行完整步骤
5.1 所需的开发环境和版本
把这套源码跑起来的前提是装好以下环境:
| 软件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 1.8及以上 | 老项目一般基于JDK8,新版本需要检查兼容性 |
| Maven | 3.6+ | 后端依赖管理 |
| Node.js | 14.x~16.x | 前端依赖安装和打包 |
| MySQL | 5.7或8.0 | 数据库 |
| Navicat或MySQL Workbench | 任意 | 数据库导入工具 |
| IDEA或VS Code | 任意 | 开发IDE |
版本这块有个容易踩的坑:如果本地装的是MySQL 8.0,驱动依赖需要使用mysql-connector-java8.x版本,连接URL里还需要加上serverTimezone=Asia/Shanghai参数,否则会报时区错误。这套源码如果用的驱动版本是5.x,连MySQL 8.0时可能需要手动升级依赖版本。
5.2 数据库初始化与后端启动步骤
第一步是创建数据库并导入SQL脚本。在Navicat里新建一个名为dental_clinic的数据库,字符集选择utf8mb4,排序规则选择utf8mb4_general_ci。然后选择"运行SQL文件",把源码里的sql/dental_clinic.sql导入进去。数据量不算大,几十秒就能导完。
第二步是修改后端配置文件application.yml中的数据源信息:
spring: datasource: url: jdbc:mysql://localhost:3306/dental_clinic?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver默认端口是8080,如果本地8080被占用,可以在配置里改成8081,相应地前端请求的baseURL也要改。
第三步是用IDEA打开后端工程,等待Maven下载完依赖(第一次拉依赖可能需要几分钟),然后启动主类DentalClinicApplication。启动成功后控制台会打印出Spring Boot的启动日志和端口号。如果启动报错,优先检查数据库连接配置和依赖下载是否完整。
5.3 前端安装依赖与启动步骤
前端工程是用Vue CLI创建的,运行步骤非常标准:
# 进入前端目录 cd dental-admin # 安装依赖(国内网络建议使用淘宝镜像) npm config set registry https://registry.npmmirror.com npm install # 启动开发服务 npm run serve开发服务默认跑在8080端口,后端也是8080,所以需要在前端开发环境配置里加一个代理。vite.config.js或者vue.config.js里配置devServer的proxy,将/api路径的请求转发到http://localhost:8080,这样前后端联调时不会遇到跨域问题。
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }浏览器访问http://localhost:8081,默认管理员的账号密码一般在README文档或SQL脚本的初始化数据里,常见的是admin/admin123或者admin/123456,源码里搜索insert into sys_user就能看到密码的MD5密文。
6. 运行过程中常见的坑与排查思路
6.1 数据库连接与时区报错
刚跑这套系统时,最容易遇到的就是数据库连接失败。典型的报错是The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,这串乱码其实是因为MySQL的时区设置不被JDBC驱动识别。解决办法有两个,一是在MySQL里执行set global time_zone = '+8:00',二是在JDBC连接URL上指定serverTimezone=Asia/Shanghai,推荐后者,因为不用改MySQL服务器配置。
还有个常见问题是用root账号默认密码登录本地MySQL 8.0时,会报Public Key Retrieval is not allowed错误。这是MySQL 8.0的caching_sha2_password认证插件导致的,连接URL上加上allowPublicKeyRetrieval=true参数就好。Navicat连接数据库时如果遇到这个问题,在连接的高级设置里勾选"允许公钥检索"。
6.2 前端跨域与接口404排查
前端页面打开后,如果登录请求发不出去,或者报了跨域错误,优先查看浏览器DevTools的Network面板,确认请求地址和后端端口是否匹配。我的排查习惯是先看请求有没有到达后端,如果Network里请求状态是CORS error,那就是跨域问题。
这套源码在后端Config里配置了CORS跨域支持,但如果前端通过Nginx反向代理部署,就需要在后端配置里把允许的域名加上,或者直接通过Nginx配置proxy_pass解决跨域。本地开发阶段最省事的方式就是5.3节说的前端代理方案。
另一个容易忽略的现象是:前端请求后端接口返回404,但接口代码明明存在。这种多半是请求路径拼接错误,前后端接口路径不一致。检查一下api目录下的请求函数里的URL写法和后端Controller的@RequestMapping是否完全对应,包括项目根路径。比如后端配置了server.servlet.context-path: /dental,前端所有请求都要带这个前缀。
6.3 Node版本过高导致依赖安装失败
前端依赖安装失败是另一个高频问题,特别是新版Node.js(17以上)和旧版本Webpack的兼容性问题。报错通常是Error: error:0308010C:digital envelope routines::unsupported,这其实是Node.js 17及以后版本OpenSSL3的默认算法变更导致的。
解决方式有几种。第一,切换Node版本到16.x,用nvm管理Node版本最方便;第二,在package.json里加一条启动脚本"dev": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve"(Windows环境);第三,升级前端构建工具到Vite或者新版Webpack。
这个坑我在好几套开源系统上都碰到过,第一次遇到时还以为是源码写错了,排查了半天发现是版本问题。所以如果你的Node版本比较新,跑不起来先别急着改代码,先看构建工具版本是否兼容。
7. 从"能跑"到"好用":几个值得继续扩展的方向
这套系统的核心功能完整、代码组织清晰,作为可用项目或者学习参考都够格。但我拿着跑通之后,对照真实诊所的运营流程,还是发现了几个可以继续深化的地方。
第一是预约提醒。现在预约功能只是记录了预约信息,没有主动通知患者。实际诊所场景里,患者经常忘记复诊时间,放鸽子率不低。可以加一个定时任务,每天早上查询当天的预约列表,给患者的手机号发一条提醒短信。Java生态里对接阿里云短信或者腾讯云短信的SDK已经很成熟了,几百行代码就能完成。
第二是影像管理。牙科诊疗非常依赖X光片和口腔扫描影像,现在这套系统还没有影像上传和查看功能。可以考虑加一个文件上传模块,把影像文件存到服务器本地或OSS,患者详情页里以时间轴方式展示不同时期的影像,方便医生对比治疗效果。前端用vue-viewer这类图片预览组件能很快实现。
第三是电子病历模板。现在病历录入是自由文本,虽然灵活但录入效率低。可以在后台配置常用的病历模板,比如"根管治疗首诊""种植牙术前检查""正畸初诊记录",医生接诊时选择模板,一键带入结构化字段,只需修改个别信息就能完成记录,能明显提升接诊效率。
第四是数据统计的可视化。当前报表模块主要是表格形式,虽然数据都有,但不直观。牙科诊所老板最关心的是每日现金流、初复诊比例、大项目(种植、正畸)收入占比这些指标。可以考虑引入ECharts,把这些指标做成趋势图和饼图,一屏就能看懂诊所的经营状况。
第五是微信小程序预约入口。现在大多数诊所都会引导患者在小程序上预约,减少前台电话预约的工作量。后端接口是现成的,小程序端复用这套系统的预约、查询接口,等于把一个管理系统的价值延伸到了患者端。
从我实际使用的角度来看,这套系统最让我满意的是它的工程规范性。虽然规模不算大,但分层清晰、命名统一、注释到位,没有那种写着写着就放飞自我的代码。对一个想把SpringBoot+Vue全栈项目学透的开发者来说,沿着患者管理到收费结算这条业务线把代码读一遍,基本就能掌握这类管理系统的全部套路。如果自己动手改一改、加点功能,踩过几个坑再填上,后端开发的实战能力也会有实打实的提升。