简介:这份资源是一份基于SSM框架的牙齿护理系统设计与研究文档,面向Java开发学习者、毕业设计选题学生以及医疗信息化方向的研究者,解决口腔护理线上预约与系统设计参考的问题。文档系统阐述了SSM(Spring+SpringMVC+MyBatis)、前后端分离模式、Vue-admin前端框架、Nginx代理及MyBatis数据库操作等核心知识点,并给出普通用户、医生、管理员、超级管理员四种角色的功能设计思路,还涵盖可行性分析、需求分析和国内外研究现状,可作为课程设计或毕业设计文档模板。压缩包仅含1个docx文件,体积约1.3MB,内容精炼集中,便于快速阅读和参考。目前已有121人学习下载,适合需要了解牙齿护理系统架构、SSM整合方案或撰写相关设计文档的读者。
1. 牙齿护理系统为什么还在用 SSM,以及这套方案解决什么问题
很多口腔诊所的前台还在用 Excel 排班,复诊提醒靠护士下班前翻纸质病历打电话。做过这类管理系统的人会有一个反直觉的体会:SSM 不是最新技术,却是能把需求最快落地的组合。Spring 管对象和事务,Spring MVC 管请求路由,MyBatis 管 SQL 与 Java 对象的转换,三个框架各管一段,出了问题能顺着调用链直接定位。下面按数据库设计、后端接口、前端联调、部署测试这条线,把牙齿护理系统的患者档案、预约排班、护理计划与复诊提醒完整拆开讲,给正在做课程设计或者准备接手类似 SSM 项目的读者一条能直接照着走的路。
2. SSM 在牙齿护理系统里的分工与核心配置
SSM 能在这类管理系统里站稳,不是因为某个框架有多强,而是三个框架的边界恰好贴合业务系统的分层习惯。Spring 的 IoC 容器负责把 Service、Mapper、事务管理器这些对象串起来;Spring MVC 聚焦 HTTP 层,把 URL、参数、JSON 和 JSP 视图之间的转换处理掉;MyBatis 把 SQL 写在 XML 里,比 Hibernate 全自动映射更容易控制分页和多表 join。牙齿护理系统恰恰是典型的多表业务:患者档案关联预约记录,预约记录关联诊疗方案,诊疗方案又延伸出护理提醒,这种场景用 MyBatis 手写 SQL 反而直观。
有人会问为什么不做 Spring Boot。如果课程设计要求拆解 SSM 配置过程,Spring Boot 的自动配置反而把核心内容藏起来了。答辩时如果能说清 web.xml、applicationContext.xml、spring-mvc.xml 的加载顺序和数据源装配方式,效果比单纯说“用了 Spring Boot”好不少。
2.1 工程目录与依赖坐标
搭这套系统时,一般会按 controller / service / mapper / entity 四层分包,配置文件放在 resources 下,JSP 放进 webapp/WEB-INF/views。目录结构本身没有标准答案,但要把 web.xml、两份 spring 配置和 mybatis 配置分开,否则后面改数据源或加拦截器时会互相干扰。
src/main/java ├── com.clinic.config ├── com.clinic.controller ├── com.clinic.service │ └── impl ├── com.clinic.mapper ├── com.clinic.entity └── com.clinic.common src/main/resources ├── spring │ ├── applicationContext.xml │ └── spring-mvc.xml ├── mybatis │ ├── mybatis-config.xml │ └── mapper ├── jdbc.properties └── sql/init.sql webapp/WEB-INF ├── web.xml └── viewspom.xml 里最核心的依赖是 spring-webmvc、mybatis、mybatis-spring 和数据库驱动。Spring 版本 5.3.x、mybatis-spring 2.0.x 是一组常用搭配,JDK 8 环境下用这套能减少兼容问题。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency>mybatis-spring 的版本要和 Spring 大版本匹配,Spring 5 不要配 mybatis-spring 1.x。mysql 驱动如果是 8.x,连接 URL 里要带 serverTimezone=Asia/Shanghai,否则日期查询会差 8 小时,预约时间显示错位。
2.2 web.xml 和两份 Spring 配置各管什么
web.xml 只做两件事:加载根容器和注册前端控制器。根容器先于 DispatcherServlet 初始化,数据源、事务、Service 和 Mapper 都会被 Spring 管理起来;DispatcherServlet 加载的 spring-mvc.xml 只管 Web 层,这样 Service 层可以独立于 Web 层测试。
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>load-on-startup 设成 1,表示 Tomcat 启动时就初始化 DispatcherServlet,而不是等第一个请求进来才初始化。url-pattern 配成 / 会拦截所有请求,所以 spring-mvc.xml 里必须放行 /static/** 下的 CSS、JS 和图片。
spring-mvc.xml 开启注解驱动、扫描 controller、配置视图解析器:
<mvc:annotation-driven /> <context:component-scan base-package="com.clinic.controller" /> <mvc:resources mapping="/static/**" location="/static/" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/" /> <property name="suffix" value=".jsp" /> </bean>mvc:annotation-driven 负责注册 JSON 转换器、参数解析器和类型转换器。InternalResourceViewResolver 的前缀后缀拼起来就是 JSP 真实路径,比如 Controller 返回 appointment/list,会找 /WEB-INF/views/appointment/list.jsp。JSP 放在 WEB-INF 下可以防止浏览器直接访问,必须经过 Controller 转发。
2.3 MyBatis 与 Spring 整合的两个关键点
在 applicationContext.xml 里配置 SqlSessionFactoryBean,dataSource 指向 Druid 数据源,mapperLocations 指向 mapper XML 目录,typeAliasesPackage 让 XML 里可以直接写 resultType="Patient",不用写全限定类名。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.clinic.entity"/> <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean>mapUnderscoreToCamelCase 打开后,数据库字段 real_name 会映射到实体属性 realName,省掉大量手写 resultMap。牙齿护理系统的预约和治疗记录表下划线字段很多,这项配置能显著减少映射代码。
第二点是配置 MapperScannerConfigurer,扫描 mapper 接口。接口名、方法名和 XML 的 namespace、id 对应后,Spring 会生成代理实现,Service 里可以直接 @Autowired。
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.clinic.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>MyBatis 的 SqlSession 生命周期由 Spring 管理,不需要手动关闭。数据源参数建议放在 jdbc.properties 里,Druid 的 initialSize 设 5、maxActive 设 20,对毕业设计规模已经足够。
3. 牙齿护理系统的表设计与 MyBatis 映射
数据库设计是这个系统不被扣分的重点。先看表关系:sys_user 存登录账号;patient 存患者信息;doctor 存医生信息和擅长方向;appointment 存预约记录;treatment_record 存每次看诊的治疗内容、医嘱和费用;nursing_plan 存护理方案与下次复诊日期,提醒任务每天查这张表。
3.1 六张核心业务表
| 表名 | 核心字段 | 说明 |
|---|---|---|
| sys_user | id, username, password, role | 系统登录账号 |
| patient | id, real_name, phone, allergy_history, id_card | 患者基本档案 |
| doctor | id, real_name, dept, specialty, status | 医生信息 |
| appointment | id, patient_id, doctor_id, appt_start, appt_end, status | 预约记录 |
| treatment_record | id, appointment_id, diagnosis, treatment, fee, next_visit_date | 诊疗与医嘱 |
| nursing_plan | id, patient_id, doctor_id, plan_name, next_remind_time, status | 护理与复诊提醒 |
设计时要把 status 字段当成一等公民。预约状态用 0 待就诊、1 已完成、2 已取消、3 爽约;护理计划状态用 0 未开始、1 执行中、2 已完成。实体类不要用 String 直接存状态,用 Integer 加常量类或枚举,后面做统计时不容易写错。
牙科系统比较特殊的一点是过敏史和既往病史。这两个字段要单独放在 patient 表,不要塞进治疗记录的 description 里。后续做治疗风险提示时,一条 SQL 就能把高风险患者捞出来,按过敏原和患者姓名筛人比全文搜索高效得多。
提示:费用字段用 decimal(10,2),不要用 float,否则金额会出现精度尾差,打印结算单时容易被患者质疑。
3.2 患者详情的一对多 resultMap
患者详情页需要同时展示预约记录和治疗历史,这是一个典型的一对多查询。先查患者再循环查预约会产生 N+1 问题,数据量小的时候不明显,页面一多就慢。用 resultMap 的 collection 可以在一条 SQL 里完成聚合。
<mapper namespace="com.clinic.mapper.PatientMapper"> <resultMap id="patientDetailMap" type="Patient"> <id property="id" column="id"/> <result property="realName" column="real_name"/> <result property="phone" column="phone"/> <collection property="appointments" ofType="Appointment"> <id property="id" column="appt_id"/> <result property="apptStart" column="appt_start"/> <result property="status" column="appt_status"/> </collection> </resultMap> <select id="selectPatientDetail" resultMap="patientDetailMap"> SELECT p.id, p.real_name, p.phone, a.id AS appt_id, a.appt_start, a.status AS appt_status FROM patient p LEFT JOIN appointment a ON a.patient_id = p.id WHERE p.id = #{id} </select> </mapper>resultMap 里 Patient 主键用 id 属性,collection 用 ofType 声明集合元素类型。SQL 使用 LEFT JOIN,如果同一患者有两条预约记录,MyBatis 会根据 resultMap 的主键 id 判断是否已经创建过 Patient 对象,相同则只追加 appointment 到集合中,不会生成重复患者对象。collection 子项的 id 列一定要起别名,比如 appt_id,避免和 patient.id 冲突后前端拿到错误 ID。
如果页面只展示最近三次预约,可以在 SQL 里先用子查询取 top 3,再 join 患者表,避免一次性加载全部历史记录,后续出现慢查询时优先检查这里。
3.3 动态 SQL 筛选预约记录
预约列表页通常有三个筛选条件:医生、日期范围、状态。写三个不同 SQL 很笨,MyBatis 的 where + if 可以动态拼接,一个方法就够。
<select id="selectAppointmentList" resultType="Appointment"> SELECT a.*, p.real_name AS patientName, d.real_name AS doctorName FROM appointment a LEFT JOIN patient p ON a.patient_id = p.id LEFT JOIN doctor d ON a.doctor_id = d.id <where> <if test="doctorId != null"> AND a.doctor_id = #{doctorId} </if> <if test="startTime != null and startTime != ''"> AND a.appt_start >= #{startTime} </if> <if test="status != null"> AND a.status = #{status} </if> </where> ORDER BY a.appt_start DESC LIMIT #{offset}, #{limit} </select>where 标签会自动处理第一个条件前面的 AND,这是 MyBatis 里最容易被忽略的细节。status 等于 0 时,if 判断status != null是成立的,所以“待就诊”状态筛选不会丢数据。日期字符串比较时,MySQL 会自动把字符串转成时间类型,但前后端格式要统一成 yyyy-MM-dd HH:mm:ss,否则会出现边界差一分钟的 bug。
这里用 LIMIT 手写分页,简单项目足够。如果条件继续变多,可以引入 PageHelper,但要检查它生成的 count 语句在慢查询日志里是否异常,它的 count 会去掉 order by,但不会自动去掉多余的 left join。
4. 预约接口与护理计划的事务实现
预约业务是牙齿护理系统里最容易出并发问题的地方。很多同学会把可约时段先查出来展示给前端,用户考虑几分钟后提交,后台却没有占用该时段,结果两个人同时预约了同一个医生的同一个时间段。
4.1 预约接口设计与参数绑定
接口按 JSON 风格设计,预约创建用 POST /api/appointment/create,取消预约用 POST 加参数。不要用 GET 做写操作,GET 会被浏览器缓存和预加载,重复提交后患无穷。
@RestController @RequestMapping("/api/appointment") public class AppointmentController { @Autowired private AppointmentService appointmentService; @PostMapping("/create") public Result<Void> create(@RequestBody @Valid AppointmentCreateDTO dto) { appointmentService.createAppointment(dto); return Result.success(); } @PostMapping("/cancel") public Result<Void> cancel(@RequestParam Integer appointmentId, @RequestParam String cancelReason) { appointmentService.cancelAppointment(appointmentId, cancelReason); return Result.success(); } }@RestController 是 @Controller 和 @ResponseBody 的组合,返回值会通过 Jackson 序列化成 JSON。DTO 里用 @NotBlank、@NotNull 做基础校验,实体类不要直接作为 Controller 入参,因为前端传来的字段里可能带 role、status 等不该由用户提交的字段。
日期字段建议加@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8"),否则 Jackson 默认序列化成时间戳,前端<input type="datetime-local">拿到数字会直接解析失败。
4.2 Service 层事务边界与防重复预约
预约的检查与插入必须在一个事务里。只做查询再插入,不做数据库约束兜底,并发测试一压就会露馅。
@Transactional(rollbackFor = Exception.class) public void createAppointment(AppointmentCreateDTO dto) { Patient patient = patientMapper.selectById(dto.getPatientId()); if (patient == null) { throw new BusinessException("患者档案不存在"); } int count = appointmentMapper.countByDoctorAndTime( dto.getDoctorId(), dto.getApptStart(), dto.getApptEnd(), AppointmentStatus.WAITING); if (count > 0) { throw new BusinessException("该时段已被预约"); } Appointment appointment = new Appointment(); BeanUtils.copyProperties(dto, appointment); appointment.setStatus(AppointmentStatus.WAITING); appointmentMapper.insert(appointment); }@Transactional 默认只回滚 RuntimeException,rollbackFor 设成 Exception.class 后,BusinessException 这类业务异常也会触发事务回滚。count 加 insert 在并发下仍有间隙,所以还要在 appointment 表上建唯一约束,用 doctor_id、appt_start 和 status 的组合作为唯一键,第二个请求插入时抛出 DuplicateKeyException,事务跟着回滚,前端会收到“该时段已被预约”的提示。
BusinessException 放在 common 包统一维护错误码。不要把 Exception 的 message 直接返回给前端,否则 SQL 报错信息会暴露表结构和连接串。
4.3 参数校验放在 Controller 还是 Service
@Valid 只适合做字段级校验,比如电话必填、时间格式。像“该时段已约满”“患者正在治疗中”这类跨表校验必须放 Service 层,因为 Controller 拿不到完整上下文。
取消预约时也建议做状态机控制,只在 WAITING 状态允许取消,已完成或已取消状态不允许二次操作。把状态流转收拢到一个方法里,前端和 Controller 不直接 setStatus,后续加需求时只需要改 Service 内部。
5. 前端页面与系统联调:把预约信息送到后端
前端不打算用 Vue 的话,JSP 加 Bootstrap 是 SSM 最省事的组合。公共头部和导航栏用 include 抽出来,页面只保留自己的内容块。预约页面里,患者和医生信息在用户登录后由后端放进 ModelAndView,前端下拉框直接渲染。EL 表达式在 Servlet 3.0 之后默认开启,用 JSTL 时注意状态码比较要写${appointment.status == 0},Integer 会自动拆箱。
5.1 Ajax 提交预约表单
传统 form 提交会刷新页面,错误提示不友好。用 Ajax 提交可以保持页面不刷新,后端返回 Result 对象带 code 和 message,前端统一弹提示。
$('#appointmentForm').on('submit', function (e) { e.preventDefault(); $.ajax({ url: ctxPath + '/api/appointment/create', type: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ patientId: $('#patientId').val(), doctorId: $('#doctorId').val(), apptStart: $('#apptStart').val(), apptEnd: $('#apptEnd').val(), description: $('#description').val() }), success: function (resp) { if (resp.code === 0) { location.reload(); } else { alert(resp.message); } }, error: function (xhr) { console.error(xhr.responseText); } }); });ctxPath 通过 JSP 里的var ctxPath = '<%=request.getContextPath()%>';获取,避免请求路径少了项目名而 404。contentType 必须是 application/json;charset=UTF-8,否则后端 @RequestBody 接不到数据。success 回调里先判断业务 code 再决定刷新或提示,HTTP 200 只能说明请求到了后端,业务失败也可能返回 200,只是 code 非 0。
5.2 联调时的三个常见问题
第一个是 404 加 500。路径对但 404 时,检查 DispatcherServlet 的 url-pattern 是否覆盖,以及 Controller 是否漏掉 @ResponseBody;500 时把xhr.responseText打印到浏览器控制台,多半是 Jackson 序列化失败。
第二个是中文乱码。Tomcat 8 以上对 POST 的 UTF-8 处理需要在 web.xml 配 CharacterEncodingFilter,并且把 forceEncoding 设为 true,否则 JSON 里的中文会变成问号。
第三个是日期格式。前端datetime-local提交的值是2025-03-20T10:30,后端 DTO 用@JsonFormat后直接转 LocalDateTime 会报错,建议在 DTO 里用 String 接,再由 Service 层统一转成 Date 或 LocalDateTime,这样前端格式错误时能给出明确提示。
6. 部署 Tomcat 与三个容易被忽视的加分细节
6.1 Maven 打包与部署
项目确认可运行后,在项目根目录执行:
mvn clean package -DskipTests命令会跳过单元测试并打包 war。war 文件名由 pom 中 finalName 决定。把它复制到 Tomcat 的 webapps 目录,启动后访问 http://localhost:8080/项目名/。如果部署后静态资源 404,先确认 war 解压目录下 webapp/static 是否存在,IntelliJ IDEA 内置 Tomcat 和外部 Tomcat 的解压路径有差别,建议用外部 Tomcat 验证一次。
6.2 数据库与日志配置
建表语句放到 resources/sql/init.sql,在项目文档里写明执行顺序。连接参数放在 jdbc.properties,Druid 的 initialSize 设 5,maxActive 设 20,system 规模完全够用。日志用 Slf4j 加 Logback,MyBatis 的日志级别调成 DEBUG 后可以看到预编译 SQL 和被替换进去的参数值,这个技巧在排查预约时间边界问题时非常有用。
6.3 三个加分细节
第一个是密码加密。不要用 MD5 明文存储,用 BCrypt 加盐更稳妥。Spring Security 里的 BCryptPasswordEncoder 可以单独拿出来用,不引入完整安全框架。
第二个是分页查询。手写 LIMIT 可行,但 PageHelper 的PageHelper.startPage(pageNum, pageSize)一行就能完成物理分页,注意它只对其后第一条 SQL 生效,中间不要插入其他查询。
第三个是复诊提醒。在配置里开启 task 注解驱动,Service 中加@Scheduled(cron = "0 0 8 * * ?"),每天 8 点扫护理计划表,把 next_remind_time 小于当天日期的记录生成提醒消息。定时任务要注意幂等,推荐在 nursing_plan 表加 remind_status 字段,任务扫描时带上remind_status = 0条件,处理完立刻更新为 1,这样即使任务重复执行或者多实例部署,也不会产生重复的复诊提醒。
本文还有配套的精品资源,点击获取