基于Java+SpringBoot的口腔牙科诊所管理系统开发实战

发布时间:2026/10/8 14:02:48
基于Java+SpringBoot的口腔牙科诊所管理系统开发实战
我先从标题拆解开始直接说这个项目。“计算机毕设 java 口腔牙科诊所管理系统 JavaSpringBoot 口腔牙科诊疗管理平台 Web 版牙科门诊就医管理系统”——名字很长但核心很清晰一套给牙科诊所用的、基于 Java 和 SpringBoot 的 Web 管理系统瞄准的是门诊就医全流程。翻译成人话就是前台能挂号、医生能写病历、收费处能结算老板能在后台看到诊所今天到底干了什么。我当年选这个题目做毕设最初觉得就是个增删改查真正动手才发现里面的业务边界、数据建模、角色权限、并发控制每个点都能单独写一篇。这篇文章适合正在做类似毕设的同学也适合想快速上手 Java 后端 Web 前端的初级开发我会把从需求拆解到落地的完整思路、关键代码和踩坑经验一起讲清楚不是贴一堆截图完事那种水文。1. 项目整体拆解口腔诊所管理系统到底在管什么1.1 核心业务场景牙科诊所不是医院但门急诊概念一个不少。我接过真实诊所的需求之后把业务分成了四块患者端由线下前台办理负责建档、挂号、排队候诊医生端负责看诊、写病历、开处置计划、开药和材料财务端负责划价收费、退费、日结统计管理端负责员工账号、角色权限、科室排班和基础数据维护。这四块看起来传统但真正做起来会发现很多操作是互相强关联的不是简单四个页面各管各的。除了这四个常规模块还有一个隐性需求特别重要复诊管理。口腔科最典型的特点是复诊周期长补牙、根管治疗、正畸都需要多次复诊。如果系统只做“一次性门诊”模型复诊患者来了只能重新建档、重新挂号、重新写病历信息全散掉。所以我的设计里单独加了一个“治疗计划”概念第一次初诊建立计划之后每次复诊都挂在计划下面医生一眼能看到这个患者做到第几步。这个点非常讨答辩老师喜欢因为它是从真实业务里提炼出来的不是从网上抄的千篇一律的 CRUD 需求。1.2 技术选型与架构设计技术选型直接决定后面的工作量和答辩难度。我的原则是既要看得出我懂框架又别给自己挖坑。最终确定的核心栈是 JDK 8 Spring Boot 2.7.18 MySQL 8.0 MyBatis-Plus 3.5.3。为什么不直接上 Spring Boot 3因为很多学校机房和新手的 Java 环境还是 JDK 8Spring Boot 3 强制要求 JDK 17版本对不上启动就报错对毕设来说太容易卡在中途。2.7 的维护期足够长资料最多网上搜到的问题基本都能对应上。后来我看网上不少人说“springboot版本太高导致各种兼容问题”就是这个原因。权限认证我选了 JWT 拦截器没有搬 Spring Security 全家桶。因为系统用户就三类角色走路径拦截已经足够JWT 做无状态登录前端拿到 token 存起来后端不用写 session讲起来也清楚。前端用了 Vue3 Element Plus用 Vite 构建。虽然用 Thymeleaf 会更简单但做毕设时前端好看很重要Vue3 的组件化和响应式更符合现代 Web 项目审美。整体是前后端分离架构Spring Boot 只提供 JSON 接口Vue 项目独立跑开发时后端在 9090前端在 5173通过代理转发避免跨域。我对比过全用 Thymeleaf 或者 JSP前者显得老后者容易翻车前后端分离虽然部署麻烦一点但答辩时很加分。2. 功能模块设计与数据库建模2.1 患者就诊主流程这个系统的主流程是建档、挂号、候诊、写病历、处置收费、发药或约复诊。新手上来最容易犯的错就是直接画表然后对着表写代码。正确的顺序应该是先走一遍流程把每一步需要的动作列清楚再倒推表结构。我举个例子挂号这一步前端页面实际上做了三个动作第一校验患者 ID 是否存在不存在就先建档第二查询医生排班表找到指定时间段是否还有剩余号源第三生成挂号单同时扣减号源。如果拆成三个接口让前端分别调用很容易出现中间断了、号源扣了但挂号没成功的问题。所以我把挂号收敛成一个带事务的后端接口createRegistration一次调用完成校验、写入、扣减号源。挂号之后患者状态变成“待就诊”。医生工作台只拉取待就诊列表选一个人点进去开始写病历。这里有一条关键业务规则如果医生没提交病历患者不能进入收费环节。我是在后端接口里写状态校验的不信任前端的按钮禁用。前端代码可以被绕过而后端校验才能真正保证业务流程不跑偏。做这个设计时付出的额外工作量并不大但在答辩时我会刻意强调这一点因为它把“业务规则由后端掌控”这个工程概念体现得很清楚。2.2 医生排班与预约模块排班是这个系统最容易翻车的部分。我一开始把排班设计成字段枚举比如上午、下午结果复诊患者想约“周四下午三点”根本没法精确。后来改成时间段表把一天切成可配置时间段比如 8:00-8:30、8:30-9:00每个时间段对应一个号源池。实体关系是这样的科室一对多医生医生一对多排班排班一对多时间段每个时间段有总号数和已约号数。患者挂号时只操作时间段的已约号数加一同时生成一条挂号记录。口腔科线上预约会有并发问题吗真实诊所一般不会像挂号平台那样秒杀但演示时老师可能会反复点同一个号源或者你测试时开了两个页面同时操作所以我还是上了乐观锁。时间段表加一个version字段更新已约号数时带上版本号如果更新影响行数为 0就告诉用户“该时段已经被约满”。这样实现简单不需要分布式锁也能避免超卖。如果你要做得更好看一点还可以在schedule_slot上加唯一索引保证同一患者不能重复预约同一时间段不过我们最终靠代码里的查询判断也够用索引属于锦上添花。2.3 收费结算与材料管理牙科收费跟普通门诊不一样治疗项目往往同时包含材料费和处置费。比如根管治疗既要体现操作费还要体现根管填充材料费。所以我设计了“收费单 收费明细”两张表主表记录总额、患者、收费员、时间明细表每行是一个项目包含项目名、单价、数量、金额。前端录入明细时可以实时累加计算但数据库落地时金额一律用 BigDecimal。我实际测试踩过 double 的坑两个浮点数相加会出现 0.30000000000000004这在财务系统里是事故级错误答辩时讲出来会显得你很有经验。另一个关键点是状态联动。一张收费单有未支付、已支付、已退费三个状态。已支付的单子不能直接删除只能走退费流程退费时生成一笔负数明细这样财务报表才能对上。这个规则是真实诊所会计跟我说的很多网上的仿电商项目只做物理删除删完账永远平不了。我把退费流程做成独立模块记录退费原因、操作员、原收费单号并在收费主表上标记原单状态。这个模块不大但是整个系统财务安全的护城河。2.4 数据库表结构与关系这里给出我最终落地的核心表结构不贴完整 SQL但把关键字段列出来照着可以自己建表。表名核心字段说明departmentid, name, description科室表doctorid, dept_id, name, title, avatar医生表patientid, name, gender, id_card, phone, address患者档案scheduleid, doctor_id, date, start_time, end_time, status排班主表schedule_slotid, schedule_id, slot_time, total_num, booked_num, version号源registrationid, patient_id, doctor_id, slot_id, status, create_time挂号单medical_recordid, registration_id, patient_id, doctor_id, chief_complaint, diagnosis, treatment_plan, create_time病历treatment_planid, patient_id, doctor_id, name, total_steps, current_step, status治疗计划charge_orderid, registration_id, patient_id, total_amount, status, create_time收费单charge_order_itemid, order_id, item_name, price, quantity, amount收费明细sys_userid, username, password, role系统账号用户表和其他表怎么关联我特意没把 patient 和 sys_user 强绑定因为真实诊所里系统账号只属于医生和管理员患者由前台代建档案不需要登录。角色分成 ADMIN、DOCTOR、RECEPTIONIST 三类权限用路径拦截。数据库命名统一用下划线实体类上写明TableNameMyBatis-Plus 默认驼峰转下划线后映射很顺畅。我建议你在答辩前把核心表的 ER 图画出来不用很漂亮但字段关系要讲得通这是数据库设计能力最直观的证明。3. 从零搭建 SpringBoot 后端核心实现3.1 项目工程结构建议包结构如下com.dentalclinic ├── config # 配置类CORS、拦截器、MyBatis-Plus分页 ├── controller # 接口层只做参数校验和结果返回 ├── service # 业务层事务和核心逻辑都在这 ├── mapper # MyBatis-Plus Mapper ├── entity # 数据库实体 ├── dto # 前端传参对象避免实体直接暴露 ├── vo # 返回给前端的视图对象 ├── common # 统一返回结果 R、错误码、异常处理 └── utils # JWT、日期工具等这个结构不是随便分的。很多同学喜欢一个 controller 写所有逻辑看着前期速度很快一旦要加功能或者答辩老师问“业务规则在哪个层”立刻露怯。分层之后controller 只是个壳service 专门管业务规则事务边界也在 service 上圈。加功能的时候只要底层 mapper 提供数据上层就能接到新接口改起来也不会牵一发动全身。对一个管理系统来说这种维护性比花哨技巧重要得多。3.2 登录鉴权与角色权限登录我用 JWT。后端登录成功生成 token里面只放 userId、username、role不放大对象。前端每次请求在 header 里带Authorization: Bearer token。后端写一个JwtInterceptor拦截/api/**放行/api/auth/login其他接口都先校验 token。校验通过后把 userId 和 role 放进 ThreadLocal业务代码随时取。这个 ThreadLocal 在请求结束后必须清理我是在拦截器的afterCompletion里调用remove()避免线程池复用导致用户信息串号。权限控制我在接口上写自定义注解RequireRole({DOCTOR,ADMIN})拦截器里通过 HandlerMethod 拿到注解判断当前角色是否匹配。这样权限规则跟接口放在一起改起来直观。比如写病历接口只允许 DOCTOR挂号接口允许 RECEPTIONIST 和 DOCTOR。有一点必须注意JWT 密钥至少 32 位别用 “123456” 这种HS256 签名算法很容易被穷举答辩老师一眼也能看出安全设计不行。如果你要做严谨一点还可以给 token 加一个过期时间一般设 2 小时前端收到 401 就跳回登录页。3.3 MyBatis-Plus 数据访问与分页数据访问直接用 MyBatis-Plus 的 BaseMapper单表操作不用写 SQL复杂查询用Select或者LambdaQueryWrapper。系统里最常见的场景就是条件分页查询比如按患者姓名查挂号记录、按状态查待就诊列表。分页必须先配置 MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }很多人查出来的 Page 对象 total 恒为 0不是代码写错就是漏了这个拦截器。另外网上常有人问“MyBatis-Plus 能不能根据实体类生成建表 SQL”其实它本身不内置这个能力想省事可以用代码生成器生成实体和 Mapper建表语句还是自己写 SQL 最可控。我用的是 IDEA 的 Database 面板直接连数据库执行建表脚本比在代码里维护一堆CREATE TABLE更方便。3.4 关键接口实战挂号和病历录入挂号接口核心步骤如下校验参数患者 ID、排班时间段 ID。查患者是否存在不存在抛业务异常。查号源时间段判断booked_num total_num。乐观锁更新booked_num booked_num 1, version version 1条件是 id 匹配且 version 等于旧值。更新成功才插入挂号记录。插入失败要回滚号源否则会出现“号被占了但挂号单没生成”。整个方法加Transactional(rollbackFor Exception.class)。很多同学只写Transactional默认遇到非运行时异常不会回滚业务里抛的是自定义异常时就留下脏数据。这个细节在答辩老师眼里就是“有没有真正理解事务”。病历录入也有自己的业务规则。医生从待就诊列表进入先根据挂号 ID 查出患者信息然后填主诉、现病史、诊断、治疗计划。如果勾选了复诊还要更新治疗计划表的 current_step。这个动作同样是一个事务病历表插入成功计划表当前步骤也要更新成功不能只做一边。我做这套逻辑时用了一个事务服务方法内部先写病历再更新计划最后把挂号状态改成“已就诊”。一次方法调用做完保证原子性。4. 前端页面与联调体验优化4.1 技术选型Thymeleaf / Layui 还是 Vue3我一开始用的是 Thymeleaf Bootstrap页面能用但看诊记录的弹窗、收费明细的累加计算用原生 JS 写起来很痛苦。后来换成 Vue3 Element Plus体验完全不一样。时间紧张的话至少用 Vue2 Element UI资料多、问题好搜。如果学校要求前后端不分离也可以用 Thymeleaf但写交互时引入 Vue 的 CDN 做局部组件化能省不少力气。Vue3 项目用 Vite 创建比 Webpack 快很多。路由我用 vue-router状态管理用 Pinia。其实这个项目不太需要复杂状态但登录后的用户信息放 Pinia 里刷新页面时再请求一次currentUser接口回填体验非常顺。如果你还用 Vuex也可以只要逻辑一致就行。关键是别把每个页面都要用的用户信息散落在各组件里不然改一个字段要翻遍整个项目。4.2 前端页面结构与关键交互页面结构上我分了五个视图登录页、首页工作台、挂号管理、医生工作台、收费管理。首页工作台放了几个统计卡片今日挂号数、待就诊数、今日收入、复诊人数。这类统计接口用 SQL 聚合很快但前端展示细节要注意我用setInterval每 30 秒刷新一次统计卡片日结页保留手动刷新按钮。这个小细节给演示带来“实时感”老师看着会觉得系统是活的而不是静态页面。挂号页面做成两步交互第一步输入患者身份证号如果没有档案就弹资料补全表单第二步选日期、科室、医生、时间段页面右侧实时显示号源剩余和医生简介。提交成功后调用window.print()打印挂号小票Chrome 下效果不错。有个热词是“web页面pdf打印”如果想做成 PDF 下载前端的思路是 html2canvas 截图转 PDF或者后端用模板引擎生成 PDF但对毕设来说没有必要调系统打印对话框完全够用还省去了环境依赖。4.3 前后端联调中的坑联调踩过的第一个坑是跨域。Vite 开发服务器默认在 5173后端在 9090直接请求会被浏览器拦截。我的解决办法是后端加 CORS 配置同时 Vite 里配置代理把/api前缀的请求转发到后端。Vite 配置如下server: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } }这样前端所有axios请求都写相对路径/api/xxx开发时不用关心端口部署时也不用改代码。第二个坑是时间格式。Java 后端把 LocalDateTime 默认序列化成2024-05-01T10:30:00这种格式在页面上显示很丑。我写了一个全局 Jackson 配置把 LocalDateTime 统一转成yyyy-MM-dd HH:mm。不处理的话前端每个字段都要手动格式化浪费时间。第三个坑是空值。MyBatis-Plus 查询结果里某些字段可能是 null前端直接渲染会出现空白或者报错。我做了两个约定所有列表接口返回的数组即使为空也返回[]绝不返回 null前端封装了一个格式化方法处理所有可能为空的字符串字段。从此页面的空白报错基本消失。5. 部署上线与常见问题排查5.1 本地打包与部署后端打包很简单Maven 执行mvn clean package -DskipTests生成 jar 包然后java -jar dental-clinic.jar --server.port9090。前端执行npm run build会生成dist目录直接丢给 Nginx 托管。如果部署到云服务器我的做法是让 Nginx 监听 80 端口/api转发到后端 9090其他静态资源指向前端dist。还要记住一点Vue Router 如果用了history模式刷新二级页面会 404必须在 Nginx 里加一条location / { try_files $uri $uri/ /index.html; }这个配置我从第一次部署就踩过坑不写的话页面一刷新就白屏新手会误以为后端崩了。只给老师演示的话本机双开也行但我建议买一台最低配云服务器把 MySQL 和 jar 部署上去演示时直接开浏览器就能访问老师立刻觉得项目能落地。5.2 毕设答辩高频问题准备答辩老师不会只问“这个系统有什么功能”更多会问“为什么这样设计”和“某个模块崩了怎么办”。我整理了自己实际准备过的问题为什么选 JWT 而不是 Session答前后端分离场景下 JWT 无状态、支持跨域、扩容方便但 token 泄露需要 HTTPS 和过期时间兜底。挂号怎么防止超卖答乐观锁控制号源数量并发下同一资源只有一个线程能更新成功。金额为什么用 BigDecimal答float 和 double 无法精确表示十进制小数财务运算会出现浮点误差。怎么保证患者必须先挂号才能写病历答后端在写病历接口里校验挂号状态不依赖前端按钮。病历保存和收费怎么解耦答病历保存自由只有收费状态影响后续取药取材料医生看诊流程不受影响。这些问题答明白以后整个系统在逻辑上就能自洽不会出现“页面看着很漂亮但一问底层就崩”的尴尬。5.3 从实际开发中总结的踩坑与避坑最后分享几个对我影响最大的实操细节都是免费教程不太会写的东西。第一Spring Boot 版本不要一味追新。我一开始用了 3.2因为 JDK 版本不对启动报错耽误了很久。换成 2.7.18 后兼容问题基本消失。毕设选稳定版本比自己追求新版本重要得多。第二热部署插件在后台管理系统上收益很低。我加过devtools结果每次改后端代码页面还在等编译反而多了很多干扰。后来直接删掉手动重启一次几分钟判断问题反而清晰。第三前端公共信息要集中管理。我一开始把当前登录用户的 username 分别存在几个页面里后来发现医生工作台和收费管理需要同一个字段每次都要各自请求。改成 Pinia 里存一份后页面初始化时拉一次后续全局共享代码清爽很多。第四MyBatis-Plus 的updateById默认不更新 null 字段。想清空某个字段必须用UpdateWrapper配合set(field, null)。我在退费功能里想清空备注用 updateById 半天没生效查文档才明白是字段更新策略的问题。第五异常统一处理一定要做。我写了一个GlobalExceptionHandler用RestControllerAdvice捕获所有业务异常和系统异常统一包装成统一的错误结构。没有它的时候后端一报错前端直接显示一堆英文堆栈页面白屏。加上以后用户至少能看到正常的中文提示框整个系统的可靠感高了一个档次。这些坑提前知道你就不用像我一样在演示前一天才手忙脚乱地填窟窿了。