ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue+MySQL构建口腔门诊管理系统:从需求到答辩的全流程实战

SpringBoot+Vue+MySQL构建口腔门诊管理系统:从需求到答辩的全流程实战

先说结论:如果你正在准备计算机毕业设计,又恰好选了医疗信息管理系统这个方向,SpringBoot + Vue + MySQL 这套组合,是我个人认为最稳妥、也最容易被答辩老师认可的技术栈。口腔医院就诊管理系统听起来好像只是“挂号 + 病历”两个功能拼在一起,但实际上手之后才发现,真正的难点几乎都集中在业务规则和边界情况上。

这篇文章我会把整个项目从需求拆解、技术选型、数据库设计、核心代码实现到排错思路完整过一遍。内容是围绕我个人实际搭过的一套口腔门诊就诊管理平台来写的,项目规模不大不小,正好适合做本科毕业设计,也能当成中小型牙科诊所的信息化改造参考。

如果你没有接触过医疗类管理系统,也不用紧张,我会把每个关键设计的原因都解释清楚,包括为什么表要这么建、为什么预约要用事务、为什么权限要分角色而不是简单地判断登录状态。看完之后哪怕你自己从零开始写,也有一个非常清晰的路线。

1. 项目整体设计与需求拆解

1.1 口腔门诊的核心痛点在哪

口腔门诊和综合医院的门诊系统有区别。综合医院往往是“大而全”,科室多、医生多、检查多,系统重点在分诊和检验数据流转。而口腔门诊的业务更集中,主要围绕预约、复诊、治疗记录、收费这几个环节打转。

我调研了本地几家口腔诊所的实际流程之后,发现它们最普遍的痛点是三个:

第一,预约全靠电话和前台手写登记,医生排班一变,患者到了现场才知道自己的预约被挤掉了,体验很差。第二,纸质病历分散存放,复诊时医生要翻半天才能找到上一次的治疗记录,尤其根管治疗、正畸这类持续性治疗,记录不连贯会导致治疗方案衔接出问题。第三,收费和诊疗记录分离,月底对账要人工比对,经常出现漏收、错收的情况。

这个系统要解决的就是这三个问题。核心定位不是做一套“医疗机构级”的HIS系统,而是做一个让小型口腔门诊能真正用起来的管理工具。

1.2 角色与核心业务流程梳理

我把系统用户划分成四类:系统管理员、医生、护士/前台、患者。没有把患者注册做得特别复杂,因为口腔门诊的患者通常到店后由前台协助建档,但线上预约要求患者自己注册账号,这两件事在需求层面是并行的。

角色权限的划分直接影响数据库设计和后端接口设计。我用了比较稳妥的RBAC模型,没有做细粒度的菜单权限,而是按角色直接分配可访问的接口范围。

角色核心权限典型操作
系统管理员全部权限医生管理、排班管理、数据统计
医生诊疗相关查看预约、填写病历、查看历史记录
护士/前台业务协助患者建档、挂号收费、排班确认
患者自助操作在线预约、查看本人记录

整个核心业务流程是:患者注册登录 → 查看医生排班 → 选择时间段预约 → 医生确认/系统自动确认 → 患者到店 → 医生填写病历 → 收费 → 完成。这个流程实际上也是数据库外键关系的蓝图,后面建表时就是围绕这条主线展开的。

1.3 技术选型的取舍逻辑

后端用SpringBoot 2.x,不用3.x。原因很简单:SpringBoot 3要求JDK17,部分学校机房和低配服务器跑起来吃力,而且大量教学资料、网上教程都基于2.x,遇到问题更容易查到解决方案。JDK用的是1.8,稳定、生态成熟,对毕设项目来说完全够用。

前端选了Vue 2 + Element UI。有人可能觉得Vue 3都这么成熟了为什么不直接上Vue 3。我的考虑是:Vue 2配合Element UI组件库在管理系统这块非常成熟,查任何“下拉框、表格、日期选择器”组合的问题,都能直接在社区找到现成的写法。Vue 3的Composition API当然好,但对毕业设计来说学习成本高,而且可能把自己困在“我今天非要搞懂setup语法”的泥潭里。

数据库用MySQL 5.7,ORM用MyBatis-Plus。MyBatis-Plus最方便的是自带分页插件和条件构造器,写简单的CRUD几乎不用手写SQL,我后面会把代码拿出来讲。认证授权用JWT,没有引入Spring Security那套重框架,而是自己写拦截器做Token校验。这个会在第四章详细说原因。

2. 系统架构与数据库设计

2.1 前后端分离架构搭建

项目采用标准的前后端分离结构,前端跑在Nginx或者直接Node环境,后端用SpringBoot内嵌的Tomcat。前后端通过JSON格式交互,接口前缀统一为/api。

后端包结构我按“controller → service → mapper → entity”分层,没有额外加DTO/VO层,因为项目规模不大,加太多层反而拖慢开发速度。但Controller层不写业务逻辑这个底线我守住了,所有事务性操作全部下沉到Service层。以后答辩老师问你“为什么这么分层”,你也能给出合理的回答:解耦、可测试、职责单一。

前端目录结构也不是随手乱建的。api目录统一放axios请求封装,views目录按功能模块建文件夹,router目录配好路由守卫。路由守卫这个细节很重要,因为前端页面必须根据登录状态和角色做访问控制,不然用户可以手动输URL跳转到管理员页面。

2.2 核心数据表设计与关系

这是整个项目的灵魂。很多同学数据库建表随手建,到写SQL和业务层时才发现关联关系理不清,回过头再改表结构,浪费大量时间。

我按业务主线设计了7张核心表,外加2张基础配置表。这里把每张表的作用和关键字段列出来,直接对照着建库就行。

表名用途关键字段
user登录账号username, password, role, status
patient患者信息user_id, name, gender, phone, allergy_info
dentist医生信息user_id, name, title, specialty, status
schedule排班记录dentist_id, work_date, start_time, end_time, total_slots, booked_slots
appointment预约挂号patient_id, schedule_id, appt_date, appt_time, status
medical_record病历记录appointment_id, patient_id, chief_complaint, diagnosis, treatment_plan
bill收费单patient_id, item_name, amount, status, pay_method

user表是账号主体,patient和dentist都通过user_id关联到user表。这样做的好处是登录认证只用关注user表,而具体业务数据分散在patient和dentist里,不会互相污染。

schedule和appointment是预约闭环的核心。我特意在schedule表里设计了total_slots和booked_slots两个字段,前者表示这个时段总共能约几个人,后者表示当前已约人数。每次预约成功,booked_slots加1,当两者相等时不能再约。这个看似简单的设计,实际上解决的是号源控制的问题,比单独记录一条条预约再统计人数要高效得多,而且写SQL判断余量时非常直观。

2.3 表结构设计的关键细节

建表时有些细节很容易踩坑,我逐个说。

第一,金额字段一律用DECIMAL(10, 2),千万不能用FLOAT或DOUBLE。浮点数在Java和MySQL中都有精度丢失的问题,涉及钱的计算必须用定点数。别问我怎么知道的,我第一次写的时候用了Double,后来发现0.1加0.2等于0.30000000000000004,当场血压拉满。

第二,所有表都带上create_time和update_time字段。一方面,做数据统计时能按时间范围筛选;另一方面,答辩和实际运维排查问题时,能清楚看到数据是什么时候写入或变更的。

第三,status字段要有默认值并配合枚举值说明。比如appointment表里status用0表示待确认、1表示已确认、2表示已完成、3表示已取消,用户表的status用0表示启用、1表示禁用。用数字存状态比用字符串好,查询快且不易出现大小写不一致的问题。

第四,逻辑删除代替物理删除。删除排班记录时,如果物理删掉,这周的统计报表数据就会缺失。所以排班表、预约表、医生表都增加deleted字段,MyBatis-Plus自带逻辑删除支持,用起来非常方便,只是一个注解的事。

3. 核心功能实现与实操要点

3.1 用户登录与权限控制

登录模块用JWT做无状态认证。用户提交用户名密码后,后端校验密码是否正确,这里必须强调一点:密码不能用明文存储,必须用BCrypt加密。Spring Security里的BCryptPasswordEncoder可以单独引入,不用引入整个Security框架。

JWT生成时把用户ID和角色写进Token里,过期时间我设置为24小时。为什么定24小时?太短的话患者用着用着就掉线,体验差;太长的话有安全隐患,一天一过期,前端配合拦截器自动刷新,这个时间长度在毕设场景里最稳妥。

拦截器实现是核心。我自己写了一个HandlerInterceptor,在preHandle方法里从请求头拿Token,解析失败就返回401。这里有个坑:拦截器会拦截所有/api/**路径,包括登录接口本身,所以要通过路径判断放行登录、注册等白名单接口。我用一个简单的ArrayList存放白名单路径,然后contains判断,不用引入复杂的权限框架,代码量不多,逻辑还非常清晰。

角色控制的校验在拦截器里一起做。用户登录后,后端接口如果需要特定角色才能访问,我就在Controller方法上加自定义注解,然后拦截器判断Token里的角色是否符合要求。比如添加医生的接口只有管理员能调,患者登录后再怎么折腾也不可能走到那个方法里去。这一点在答辩演示时要专门提,因为它是整个系统安全性的关键。

3.2 预约挂号模块的实现

预约的完整流程是,患者先看到医生的排班列表然后选择一个时间端提交预约,后端收到请求后校验这个时间段是否还有余量,有则创建预约记录并扣减号源,没有则直接返回“该时段已约满”。

这里最危险的问题就是并发超约。两个人同时看到还剩1个号,然后同时点提交,如果没有并发控制,两个请求都能读到booked_slots=1的排班记录,然后都判断还有余位,最后两张预约都创建成功,实际超约了。门诊可能不觉得这有什么,但答辩老师最喜欢问的就是“你这个系统并发情况下会不会有问题”。

解决并发超约的办法有几种,我选了两种用在一个方案里:一种是SQL层面的原子更新,另外一种是事务隔离。

第一层是更新时带上条件判断,update schedule set booked_slots = booked_slots + 1 where id = ? and booked_slots < total_slots,如果返回的影响行数为0,说明号源已经满了,直接返回预约失败。这一条SQL同时完成了扣减号和判断余量的操作,天然是原子的,避免了先查再更新的时间差问题。

第二层是加@Transactional注解包裹整个预约流程。创建预约记录和更新号源必须在同一个事务里,任何一个失败都要全部回滚,否则会出现预约记录生成了但号源没扣减成功,或者号源扣了但预约记录没建的情况。我用了一个测试用例验证:先在两个浏览器同时操作同一个号源,发现没有事务和原子更新时会超约,加上之后只有一条预约能成功,另一条被正确拒绝。

3.3 就诊记录与电子病历模块

病历是整个口腔门诊系统的信息核心。一张病历记录至少要和预约记录关联,这样才能追溯这次就诊对应的是哪次预约、哪个医生、哪个时间段。我设计的medical_record表里专门保存appointment_id,通过预约记录再关联患者和医生,形成一条完整的数据链。

填写病历时无非是主诉、诊断、治疗方案、医嘱这几块内容。口腔门诊有别于综合医院的是,它的治疗过程经常跨多次就诊,比如根管治疗要做两三次,正畸要按月复诊。我做的方案是,同一患者的病历记录按就诊时间排列,患者详情页中可以把历史病历列成时间线,医生一点就能看到上次做什么、下次该做什么。

还有一个小小的贴心设计,在患者信息里增加allergy_info字段,专门记录过敏史。口腔治疗中麻药、抗生素等药物过敏是真实风险,这个字段在病历页面顶部高亮显示,医生打开病历先看到过敏史再动治疗方案,这在实际诊所场景里是有价值的,不只是为了摆功能。

3.4 医生排班模块

排班功能是预约的前提。没有排班,患者就不知道该约哪个时间段。我把排班设计成按天生成,管理员进入排班页面,选择医生、选择日期,然后设置这个医生的出诊开始时间和结束时间,再设置每个时段可预约的人数。

从业务逻辑上来讲,排班有两点要注意。第一,同一医生同一天不能重复排班,这个约束在数据库层面用唯一索引实现,在应用层面也要加判断,我实际两个地方都做了,唯一索引兜底,应用层提示友好。第二,修改排班时如果已有预约记录,不能直接删掉这个排班,只能把状态改成“已停诊”,同时给已预约的患者发送提醒,这是我在和诊所沟通时学到的一个细节,看起来小但很体现系统完善度。

生成排班我做得比较简单,就是固定把一天按小时切成时间段。如果你时间充裕,可以让管理员自定义时间段长度,比如上午8点到12点,每30分钟一个号源,但这会让后端逻辑复杂不少。按小时切是一种简洁且演示效果不差的折中方案。

3.5 收费与统计模块

收费模块说简单也简单,说复杂也复杂。简单在于就是记录一个费用明细,复杂在于它要和就诊记录绑定。我的方案是,医生完成病历填写后,可以发起收费请求,填写收费项目名称和金额,生成一张收费单,状态为待支付。前台在收费页面可以看到所有待支付的单子,点击确认收款后状态变成已支付。

全部费用数据沉淀到bill表之后,做统计就方便了。管理员的首页数据面板展示三个核心指标:当日预约量、当日就诊量、当日总收入。这三个指标分别从appointment表和bill表统计,用SQL的聚合查询就能搞定。

我用了ECharts做可视化,柱状图展示最近七天的收入变化,饼图展示不同收费项目占比。答辩的时候,数据可视化面板放在首页最显眼的位置,演示效果很加分。统计接口要注意日期格式化的问题,前端传日期范围时统一用yyyy-MM-dd,后端拿LocalDate解析,不要用Date,旧API真的太容易踩时区坑了。

4. 关键代码实战与配置说明

4.1 项目配置与启动类

创建SpringBoot项目后用application.yml统一管理配置。我贴一份基础配置,你直接用,把数据库账号密码改一下就能跑起来。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/dental_clinic?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这段配置里mybatis-plus部分两个细节值得说。log-impl用StdOutImpl可以在控制台直接打印完整SQL语句,排查问题时作用非常大,比DEBUG断点来得快。map-underscore-to-camel-case是让数据库的user_name自动映射到实体的userName,省去一堆@TableField注解。逻辑删除三行配置是全局的,实体类字段标上@TableLogic就自动生效。

4.2 核心Service层代码示例

预约接口是系统的重中之重,我把最关键的一段Service方法贴出来讲。这段代码是前面说的并发控制方案的具体实现。

@Override @Transactional(rollbackFor = Exception.class) public Result createAppointment(AppointmentCreateDTO dto) { Schedule schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule == null) { return Result.error("排班不存在"); } if (schedule.getStatus() != 0) { return Result.error("排班已停诊,无法预约"); } if (StrUtil.isNotBlank(dto.getAppointmentTime()) && !isTimeInRange(dto.getAppointmentTime(), schedule)) { return Result.error("所选时间不在排班范围内"); } Appointment appointment = new Appointment(); appointment.setPatientId(dto.getPatientId()); appointment.setScheduleId(schedule.getId()); appointment.setDentistId(schedule.getDentistId()); appointment.setApptDate(schedule.getWorkDate()); appointment.setStatus(0); appointmentMapper.insert(appointment); int updated = scheduleMapper.increaseBookedSlots(schedule.getId()); if (updated == 0) { throw new RuntimeException("号源已满,预约失败"); } return Result.success("预约成功"); }

关键是increaseBookedSlots这个方法,它对应上面说的原子更新SQL。在Mapper里写好注解SQL就行。

@Update("UPDATE schedule SET booked_slots = booked_slots + 1 " + "WHERE id = #{id} AND booked_slots < total_slots") int increaseBookedSlots(@Param("id") Long id);

这段代码的写法建议你直接照抄,配合@Transactional控制事务。顺序上需要注意,先插入预约记录再扣号源,扣不动就抛异常让整个事务回滚。如果把扣号源放在插入之前,一旦插入预约记录失败,扣掉的号源也得手动恢复,稍麻烦一些。

4.3 前端页面与接口对接

前端代码不必全部展开,但axios拦截器的逻辑有必要说一下。每次请求前把Token从localStorage取出来,添加到请求头,这是前后端分离项目没得跑的。响应处理时,如果状态码是401就跳转登录页,避免用户Token过期后页面白屏。

页面设计这块,我建议预约页优先使用卡片式展示医生信息,排班用日历视图展示可约日期。Element UI自带日历组件el-calendar,自定义日期单元格的内容就能展示每天剩余号源。前端这块唯一的建议是不要贪多,把表格、表单、弹窗、日历这几个组件用熟,比各种花哨的动画效果有用得多。

5. 常见问题与排查技巧实录

5.1 运行时异常速查表

开发过程中踩过的坑确实不少,我把典型问题整理成一张表,比看堆栈日志来得直接。

问题表现根本原因解决思路
访问接口返回404路径写错或拦截器配置错误console看后端日志,检查请求路径和Controller映射
提交预约时总是“号源已满”排班记录booked_slots初始值不对手动修改数据库,或写接口统一重置号源
返回给前端的时间多出8小时时区不一致连接串加serverTimezone=Asia/Shanghai
删除医生后预约记录查询报错外键关联无数据或字段为null保留医生逻辑删除,查询时跳过deleted=1
JWT过期频繁过期时间太短调整为24小时并补充前端自动刷新逻辑

第一个问题背后其实是我自己栽过跟头。拦截器放行路径写错,导致注册接口都被拦截,但前端请求的是/api/auth/register,拦截器判断路径不在白名单里就直接返回401。排查半天才发现白名单里的尾斜杠问题,register和register/是一样的,但字符串contains判断偏偏不认。后来统一用trimTrailingSlash处理一遍才彻底解决。

5.2 排班和号源恢复的时序难题

排班是典型的时效性业务。修改排班时,如果当天已经有患者预约并且状态还是待就诊,直接改医生出诊时间会让患者白跑一趟。所以修改排班的Service方法里,一定要先查一下有没有关联的预约记录。

号源恢复的另一个坑是:当预约状态从已确认改为已取消时,booked_slots要减回去,但改成已完成时不能减。很多同学会漏掉这个判断,直接统一减1,结果号源越滚越不准。其实准确的做法是写一个定时任务,在每天凌晨把过期且未就诊的预约批量标记为已取消,同时恢复号源。门诊可能没有太多过期预约,但这个逻辑能防止数据越来越脏。

我用Spring自带的@Scheduled注解写了一个小任务,每天凌晨3点执行。为什么定在凌晨3点?因为门诊已经下班,这时候做数据重置不影响正常业务。

5.3 安全细节与数据展示问题

安全这块有几个点非常容易被毕业设计忽略,但答辩老师会抓着问。

第一,密码传输必须加密。虽然项目里用了HTTPS不是标配,但前端在后端接口加密传输密码,保持一致的规范是有必要的。我在前端提交表单前用JS的SHA256哈希一次密码,后端拿到哈希值后先做Base64解码再和数据库比对。你不用上全套加密方案,但至少不能在浏览器开发者工具里明文看到密码。

第二,接口返回数据要做脱敏。患者详情接口和列表接口返回同一个实体类时,如果直接把手机号全都返回,前端的网络面板里就能看到所有患者的电话,这是隐私事故。所以列表接口我做了字段裁剪,只返回姓名、性别、年龄、最近就诊时间,完整信息只有详情页才返回。

第三,SQL注入的防御。用MyBatis-Plus自带的条件构造器时,参数都是预编译的,基本安全。但如果你自己拼接SQL字符串,嘿嘿,那就该被答辩老师锤了。写原生SQL时一律用@Param传参,不要用${}去替代字段值。

6. 写在最后的实操感受

这套系统我从零开始写到最后能跑通完整的预约就诊流程,前后大概花了三周。第一周做需求梳理和数据库设计,第二周完成后端接口,第三周补前端页面和联调。如果把所有代码都复用现成模板的话,一个基础扎实的同学可以压缩到两周内完成。

我个人实际写下来最大的体会是,这套项目真正的技术亮点不在某个花哨的算法或者炫酷的界面,而在于把门诊业务规则吃透了。预约并发控制怎么用原子更新搞定,号源怎么恢复,病历怎么和预约关联成闭环,这些东西才是答辩老师真正会追问的地方。你就算把代码全部背下来了,业务逻辑讲不清楚,照样会露馅。

最后再分享一个小技巧:一定要准备一套完整的演示数据,包括一个未来三天的排班、一位有三次就诊历史的患者、几张不同状态的收费单。演示时先登录患者端约一个号,再切到医生端接诊、录病历、发起收费,最后到管理员端看统计面板数据变化。这套演示流程走下来,比你口头讲十分钟架构都管用。

如果你正准备用这个题目做毕业设计,我的建议是不要急着写代码,先花时间把表关系理清楚,把预约这条主流程在纸上画通,后面的事情就会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进