SSM框架实战:Java毕业设计会议管理系统核心实现与避坑指南
简介本资源为基于SSM框架的会议管理系统毕业设计完整项目包面向高校计算机相关专业学生及Java初学者可用于毕业设计、课程设计或期末大作业。项目采用SSM后台框架前端使用HTML/JSP/Vue数据库为MySQL开发环境为IDEA部署依赖Tomcat与Maven代码注释详尽新手也能看懂。压缩包共1068个文件约77.99MB涵盖178个Java源文件、124个Vue组件、322个SVG图标、44个XML配置及SQL脚本等前后端代码与数据库脚本一应俱全。系统功能完善、界面美观、操作简单涵盖会议管理核心业务模块经过严格调试可稳定运行。已有69人学习关注下载后简单部署即可使用适合作为高分毕设参考帮助读者快速掌握SSM整合开发流程与项目部署思路。1. 会议管理系统到底在管什么从一场跨部门周会说起很多同学做 Java 毕业设计时一看到“会议管理系统”就觉得没什么技术含量无非是增删改查。但真正动手写起来你会发现这个题目的复杂度被严重低估了。一场跨部门周会从发起预约、审批会议室、通知参会人、签到、记录纪要、跟踪待办到会后归档整条链路涉及角色权限、时间冲突检测、状态流转、消息触达等多个工程问题。基于 SSMSpring SpringMVC MyBatis来实现这套系统恰好能把 Java Web 开发的核心能力串起来控制层路由、业务层事务、持久层映射、前后端数据交互一个都跑不掉。这篇文章面向正在做 Java 毕业设计、选了会议管理系统这个题目的同学也面向想用 SSM 练手一个完整业务系统的开发者。我会把需求拆解、数据库设计、核心代码实现、常见踩坑点讲清楚让你看完能直接动手复现而不是停留在“知道有这么个东西”的层面。选 SSM 而不是 Spring Boot理由也很实际很多高校的毕设答辩环境对版本和依赖有固定要求SSM 的 XML 配置方式虽然繁琐但胜在可控、可解释答辩时你能说清楚每一层在干什么。2. 需求拆解与数据库设计先想清楚再建表2.1 三类角色与核心用例会议管理系统的角色通常分三种普通员工、部门管理员、系统管理员。普通员工能发起会议申请、查看自己被邀请的会议、签到、查看纪要部门管理员多一个审批会议申请的权限以及管理本部门会议室系统管理员负责用户管理、会议室资源管理、全局配置。核心用例可以归纳为六条主线用户登录与权限校验、会议申请与审批、会议室预约与冲突检测、参会通知与签到、会议纪要记录与归档、待办事项跟踪。这六条线不是并列关系而是有先后依赖的。比如冲突检测必须在审批通过之前完成否则会出现两个会议抢同一间会议室的情况。我一般会先画一张状态流转图不用工具纸上画就行把会议状态定义为待审批 → 已通过 → 进行中 → 已结束 → 已归档外加一个已驳回的旁支。状态流转清楚了后面写业务代码时就不会出现“这个字段到底该不该改”的犹豫。2.2 数据库表设计与字段说明数据库设计是整个系统的地基。表建得合理后面写 SQL 和业务代码会顺很多表建得随意后面到处打补丁。下面是核心表的设计方案以 MySQL 为例。用户表 t_user字段名类型说明idbigint主键自增usernamevarchar(50)登录名唯一索引passwordvarchar(128)密码存 MD5 加盐值real_namevarchar(50)真实姓名roletinyint角色1 员工2 部门管理员3 系统管理员dept_idbigint所属部门 IDstatustinyint状态1 正常0 禁用create_timedatetime创建时间会议室表 t_room字段名类型说明idbigint主键room_namevarchar(100)会议室名称capacityint容纳人数locationvarchar(200)位置描述equipmentvarchar(500)设备清单逗号分隔statustinyint1 可用0 维护中会议表 t_meeting字段名类型说明idbigint主键titlevarchar(200)会议主题room_idbigint会议室 IDinitiator_idbigint发起人 IDstart_timedatetime开始时间end_timedatetime结束时间statustinyint1 待审批2 已通过3 进行中4 已结束5 已驳回6 已归档attendee_idstext参会人 ID 列表逗号分隔descriptiontext会议说明create_timedatetime创建时间会议纪要表 t_minutes字段名类型说明idbigint主键meeting_idbigint关联会议 IDcontenttext纪要内容recorder_idbigint记录人 IDcreate_timedatetime记录时间待办表 t_todo字段名类型说明idbigint主键meeting_idbigint关联会议 IDassignee_idbigint负责人 IDcontentvarchar(500)待办内容deadlinedatetime截止时间statustinyint0 未完成1 已完成这里有一个设计决策值得展开说参会人用逗号分隔存在一个字段里还是单独建一张关联表单独建关联表t_meeting_attendee在范式上更规范查询也更灵活比如“查某人参加过的所有会议”用 JOIN 很直接。但逗号分隔的方式在插入和读取时少一次关联查询对于毕设这种数据量不大的场景够用。我的建议是如果你答辩时老师可能追问数据库范式就用关联表如果更关注开发速度逗号分隔也能接受但要在查询时注意用 FIND_IN_SET 函数。-- 建库语句 CREATE DATABASE meeting_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 用户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 1, dept_id BIGINT, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 会议表注意时间字段加索引冲突检测会频繁查询 CREATE TABLE t_meeting ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, room_id BIGINT NOT NULL, initiator_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1, attendee_ids TEXT, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_time (room_id, start_time, end_time) ) ENGINEInnoDB;上面建表语句里idx_room_time这个联合索引是专门为冲突检测查询准备的。冲突检测的 SQL 逻辑是查同一间会议室、状态不是已驳回或已归档、且时间段有重叠的记录。没有这个索引每次检测都全表扫描数据量一上来就慢。2.3 冲突检测的 SQL 逻辑时间冲突的判断条件是已有会议的 start_time 小于新会议的 end_time且已有会议的 end_time 大于新会议的 start_time。这个条件覆盖了三种重叠情况完全包含、部分重叠、被包含。SELECT COUNT(*) FROM t_meeting WHERE room_id #{roomId} AND status NOT IN (5, 6) AND start_time #{endTime} AND end_time #{startTime}参数说明roomId 是待预约的会议室 IDstartTime 和 endTime 是新会议的时间段。status NOT IN (5, 6) 排除了已驳回和已归档的会议这两种状态不占用会议室资源。返回 COUNT 大于 0 就说明有冲突需要提示用户换时间或换会议室。3. SSM 三层架构落地从 Controller 到 Mapper 的完整链路3.1 项目结构与依赖配置SSM 项目的标准结构是controller 层负责接收请求和返回响应service 层写业务逻辑和事务控制daomapper层负责数据库操作。配置文件包括 spring-dao.xml数据源和 MyBatis 整合、spring-service.xml事务和 service 扫描、spring-mvc.xmlMVC 注解驱动和视图解析器、web.xml前端控制器和监听器。Maven 依赖的核心坐标包括spring-webmvc、spring-jdbc、spring-tx、mybatis、mybatis-spring、mysql-connector-java、druid连接池、jstl、servlet-api。版本选择上Spring 用 5.xMyBatis 用 3.5.xMySQL 驱动用 8.x注意驱动类名是 com.mysql.cj.jdbc.Driver。!-- pom.xml 核心依赖片段 -- dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies这段依赖配置里druid 连接池不是必须的但用它比用 Spring 自带的 DriverManagerDataSource 要好因为 druid 提供了连接监控和 SQL 统计功能调试阶段能帮你看清楚哪些 SQL 执行慢。mybatis-spring 的版本要和 mybatis 主包版本匹配否则可能出现 NoSuchMethodError。3.2 会议申请与冲突检测的 Service 实现Service 层是业务逻辑的核心。会议申请这个操作涉及三个步骤校验时间合法性、检测会议室冲突、插入会议记录。这三个步骤必须在同一个事务里否则可能出现冲突检测通过了但插入失败、或者插入成功了但冲突检测没做完的情况。Service public class MeetingServiceImpl implements MeetingService { Autowired private MeetingMapper meetingMapper; Override Transactional(rollbackFor Exception.class) public Result createMeeting(Meeting meeting) { // 1. 基础校验结束时间必须晚于开始时间 if (meeting.getEndTime().before(meeting.getStartTime())) { return Result.fail(结束时间不能早于开始时间); } // 2. 冲突检测 int conflictCount meetingMapper.countConflict( meeting.getRoomId(), meeting.getStartTime(), meeting.getEndTime() ); if (conflictCount 0) { return Result.fail(该时间段会议室已被占用请调整时间或更换会议室); } // 3. 插入记录状态设为待审批 meeting.setStatus(1); meeting.setCreateTime(new Date()); meetingMapper.insert(meeting); return Result.success(申请已提交等待审批); } }这段代码的关键点有三个。第一Transactional(rollbackFor Exception.class)指定了回滚异常类型默认只回滚 RuntimeException加上 rollbackFor 后受检异常也会触发回滚。第二冲突检测放在插入之前但即使这样在高并发场景下仍可能出现两个请求同时通过检测然后都插入的情况这就是所谓的“幻读”问题。解决办法是在 countConflict 查询时加FOR UPDATE锁或者对会议室 ID 加分布式锁。毕设场景下并发量不大但答辩时老师可能会问这个问题提前想好答案。第三返回结果用统一的 Result 对象封装包含 code、msg、data 三个字段前端根据 code 判断成功还是失败。3.3 MyBatis Mapper XML 的编写要点Mapper XML 是 SSM 里最容易出问题的地方。常见错误包括namespace 写错导致绑定不上、resultMap 字段映射不对导致查出来是 null、动态 SQL 的 if 条件拼错导致 SQL 语法错误。mapper namespacecom.demo.dao.MeetingMapper resultMap idmeetingMap typecom.demo.entity.Meeting id columnid propertyid/ result columntitle propertytitle/ result columnroom_id propertyroomId/ result columnstart_time propertystartTime/ result columnend_time propertyendTime/ result columnstatus propertystatus/ /resultMap select idcountConflict resultTypeint SELECT COUNT(*) FROM t_meeting WHERE room_id #{roomId} AND status NOT IN (5, 6) AND start_time lt; #{endTime} AND end_time gt; #{startTime} /select insert idinsert parameterTypecom.demo.entity.Meeting useGeneratedKeystrue keyPropertyid INSERT INTO t_meeting (title, room_id, initiator_id, start_time, end_time, status, attendee_ids, description) VALUES (#{title}, #{roomId}, #{initiatorId}, #{startTime}, #{endTime}, #{status}, #{attendeeIds}, #{description}) /insert /mapperXML 里和必须转义为lt;和gt;这是新手最容易翻车的地方不转义直接报 SQL 语法错误。useGeneratedKeystrue keyPropertyid让插入后能把自增主键回填到实体对象里后面如果需要用这个 ID 关联其他表就靠它。resultMap 里 column 是数据库字段名property 是 Java 实体属性名下划线和驼峰之间的转换必须手动配或者开启 MyBatis 的 mapUnderscoreToCamelCase 配置。3.4 Controller 层与前后端数据交互Controller 层负责接收前端请求、调用 Service、返回 JSON 数据。SSM 里用 RestController 或 Controller ResponseBody 来返回 JSON。RestController RequestMapping(/meeting) public class MeetingController { Autowired private MeetingService meetingService; PostMapping(/create) public Result create(RequestBody Meeting meeting, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.fail(请先登录); } meeting.setInitiatorId(user.getId()); return meetingService.createMeeting(meeting); } GetMapping(/list) public Result list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, HttpSession session) { User user (User) session.getAttribute(loginUser); PageInfoMeeting pageInfo meetingService.listByUser(user.getId(), page, size); return Result.success(pageInfo); } }这里用 HttpSession 存登录用户信息是最简单的权限控制方式。拦截器里检查 session 中是否有 loginUser没有就跳转登录页。分页查询用 PageHelper 插件在 Service 里调PageHelper.startPage(page, size)再执行查询返回的 list 会被自动包装成 PageInfo 对象包含总记录数、总页数等信息。4. 避坑指南那些让我熬夜的报错和排查思路4.1 中文乱码问题现象前端提交的中文数据存到数据库变成问号或者页面显示乱码。原因通常有三层数据库字符集不是 utf8mb4、连接 URL 没指定字符编码、Tomcat 的 URIEncoding 没配。解决方法是逐层排查先确认建库时用了 utf8mb4再检查 JDBC URL 里加了useUnicodetruecharacterEncodingutf8最后在 web.xml 里配 CharacterEncodingFilter 强制请求和响应都用 UTF-8。4.2 事务不生效现象Service 方法上加了 Transactional但插入失败后前面的操作没有回滚。原因通常是 Spring 配置里没有开启事务注解驱动或者 Service 类没有被 Spring 扫描到。检查 spring-service.xml 里是否有tx:annotation-driven transaction-managertransactionManager/以及包扫描路径是否覆盖了 Service 所在包。另一个常见原因是方法不是 public 的Spring AOP 默认只代理 public 方法。4.3 MyBatis 绑定异常现象启动时报Invalid bound statement (not found)。原因通常是 Mapper XML 的 namespace 和 Mapper 接口的全限定名不一致或者 XML 文件没有放在 resources 目录下对应的包路径里。SSM 项目里 Mapper XML 一般放在src/main/resources/mapper/目录下需要在 spring-dao.xml 里配置mapperLocations属性指定扫描路径。4.4 时间格式前后端不一致现象前端传2024-01-15 09:00:00后端接收时报格式转换错误。原因是 SpringMVC 默认不支持这种格式的字符串转 Date。解决办法是在实体类的日期字段上加DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)注解或者在 spring-mvc.xml 里注册自定义的 Converter。返回 JSON 时用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)保证序列化格式一致。4.5 静态资源被拦截现象CSS、JS、图片加载不出来浏览器控制台报 404。原因是 spring-mvc.xml 里配置的 DispatcherServlet 拦截了所有请求包括静态资源。解决办法是在 spring-mvc.xml 里加mvc:default-servlet-handler/或mvc:resources mapping/static/** location/static//把静态资源请求交给默认 Servlet 处理。5. 进阶技巧让系统更稳、更好演示5.1 用拦截器做统一权限校验与其在每个 Controller 方法里手动检查 session不如写一个 HandlerInterceptor在 preHandle 方法里统一判断登录状态和角色权限。这样代码更干净也不容易漏掉某个接口。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }拦截器注册在 spring-mvc.xml 里通过mvc:interceptors配置拦截路径和排除路径。登录接口和静态资源要排除掉否则会死循环。5.2 会议状态自动流转会议状态从“已通过”到“进行中”再到“已结束”如果全靠人工点击按钮来改演示时容易忘。可以写一个定时任务每分钟扫描一次 t_meeting 表把 start_time 已到且状态为 2 的记录改成 3把 end_time 已过且状态为 3 的记录改成 4。Spring 的 Scheduled 注解就能实现在 spring-service.xml 里开启task:annotation-driven/即可。Component public class MeetingStatusTask { Autowired private MeetingMapper meetingMapper; Scheduled(fixedRate 60000) public void autoUpdateStatus() { meetingMapper.updateToOngoing(); meetingMapper.updateToFinished(); } }对应的 SQL 是UPDATE t_meeting SET status 3 WHERE status 2 AND start_time NOW()和UPDATE t_meeting SET status 4 WHERE status 3 AND end_time NOW()。这个技巧在答辩演示时很加分老师能看到系统有“自动运转”的能力。5.3 答辩演示的加分项演示时不要只走一遍增删改查。建议按这个顺序先用普通员工账号发起一个会议申请特意选一个已被占用的时间段展示冲突检测的提示然后换部门管理员账号审批通过再展示会议列表里状态的变化最后打开会议纪要页面写一条纪要并添加待办。整个流程走完老师能看到你对业务闭环的理解而不只是代码堆砌。另外提前准备两个问题的答案一是“如果两个人同时预约同一间会议室怎么办”回答用数据库唯一索引或悲观锁二是“密码为什么不能明文存”回答用 MD5 加盐或 BCrypt。这两个问题被问到的概率很高。我在做这类系统时养成了一个习惯每写完一个模块就手动构造三条异常数据测一下比如结束时间早于开始时间、参会人 ID 不存在、会议室已停用。这些边界情况才是真正让系统“能用”和“好用”的分界线。希望帮到你。本文还有配套的精品资源点击获取