简介:本资源为基于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 毕业设计时,一看到“会议管理系统”就觉得没什么技术含量,无非是增删改查。但真正动手写起来,你会发现这个题目的复杂度被严重低估了。一场跨部门周会,从发起预约、审批会议室、通知参会人、签到、记录纪要、跟踪待办,到会后归档,整条链路涉及角色权限、时间冲突检测、状态流转、消息触达等多个工程问题。基于 SSM(Spring + SpringMVC + MyBatis)来实现这套系统,恰好能把 Java Web 开发的核心能力串起来:控制层路由、业务层事务、持久层映射、前后端数据交互,一个都跑不掉。
这篇文章面向正在做 Java 毕业设计、选了会议管理系统这个题目的同学,也面向想用 SSM 练手一个完整业务系统的开发者。我会把需求拆解、数据库设计、核心代码实现、常见踩坑点讲清楚,让你看完能直接动手复现,而不是停留在“知道有这么个东西”的层面。选 SSM 而不是 Spring Boot,理由也很实际:很多高校的毕设答辩环境对版本和依赖有固定要求,SSM 的 XML 配置方式虽然繁琐,但胜在可控、可解释,答辩时你能说清楚每一层在干什么。
2. 需求拆解与数据库设计:先想清楚再建表
2.1 三类角色与核心用例
会议管理系统的角色通常分三种:普通员工、部门管理员、系统管理员。普通员工能发起会议申请、查看自己被邀请的会议、签到、查看纪要;部门管理员多一个审批会议申请的权限,以及管理本部门会议室;系统管理员负责用户管理、会议室资源管理、全局配置。
核心用例可以归纳为六条主线:用户登录与权限校验、会议申请与审批、会议室预约与冲突检测、参会通知与签到、会议纪要记录与归档、待办事项跟踪。这六条线不是并列关系,而是有先后依赖的。比如冲突检测必须在审批通过之前完成,否则会出现两个会议抢同一间会议室的情况。
我一般会先画一张状态流转图(不用工具,纸上画就行),把会议状态定义为:待审批 → 已通过 → 进行中 → 已结束 → 已归档,外加一个已驳回的旁支。状态流转清楚了,后面写业务代码时就不会出现“这个字段到底该不该改”的犹豫。
2.2 数据库表设计与字段说明
数据库设计是整个系统的地基。表建得合理,后面写 SQL 和业务代码会顺很多;表建得随意,后面到处打补丁。下面是核心表的设计方案,以 MySQL 为例。
用户表 t_user
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录名,唯一索引 |
| password | varchar(128) | 密码,存 MD5 加盐值 |
| real_name | varchar(50) | 真实姓名 |
| role | tinyint | 角色:1 员工,2 部门管理员,3 系统管理员 |
| dept_id | bigint | 所属部门 ID |
| status | tinyint | 状态:1 正常,0 禁用 |
| create_time | datetime | 创建时间 |
会议室表 t_room
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| room_name | varchar(100) | 会议室名称 |
| capacity | int | 容纳人数 |
| location | varchar(200) | 位置描述 |
| equipment | varchar(500) | 设备清单,逗号分隔 |
| status | tinyint | 1 可用,0 维护中 |
会议表 t_meeting
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(200) | 会议主题 |
| room_id | bigint | 会议室 ID |
| initiator_id | bigint | 发起人 ID |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| status | tinyint | 1 待审批,2 已通过,3 进行中,4 已结束,5 已驳回,6 已归档 |
| attendee_ids | text | 参会人 ID 列表,逗号分隔 |
| description | text | 会议说明 |
| create_time | datetime | 创建时间 |
会议纪要表 t_minutes
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| meeting_id | bigint | 关联会议 ID |
| content | text | 纪要内容 |
| recorder_id | bigint | 记录人 ID |
| create_time | datetime | 记录时间 |
待办表 t_todo
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| meeting_id | bigint | 关联会议 ID |
| assignee_id | bigint | 负责人 ID |
| content | varchar(500) | 待办内容 |
| deadline | datetime | 截止时间 |
| status | tinyint | 0 未完成,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 ) ENGINE=InnoDB; -- 会议表,注意时间字段加索引,冲突检测会频繁查询 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) ) ENGINE=InnoDB;上面建表语句里,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 是待预约的会议室 ID,startTime 和 endTime 是新会议的时间段。status NOT IN (5, 6) 排除了已驳回和已归档的会议,这两种状态不占用会议室资源。返回 COUNT 大于 0 就说明有冲突,需要提示用户换时间或换会议室。
3. SSM 三层架构落地:从 Controller 到 Mapper 的完整链路
3.1 项目结构与依赖配置
SSM 项目的标准结构是:controller 层负责接收请求和返回响应,service 层写业务逻辑和事务控制,dao(mapper)层负责数据库操作。配置文件包括 spring-dao.xml(数据源和 MyBatis 整合)、spring-service.xml(事务和 service 扫描)、spring-mvc.xml(MVC 注解驱动和视图解析器)、web.xml(前端控制器和监听器)。
Maven 依赖的核心坐标包括:spring-webmvc、spring-jdbc、spring-tx、mybatis、mybatis-spring、mysql-connector-java、druid(连接池)、jstl、servlet-api。版本选择上,Spring 用 5.x,MyBatis 用 3.5.x,MySQL 驱动用 8.x(注意驱动类名是 com.mysql.cj.jdbc.Driver)。
<!-- pom.xml 核心依赖片段 --> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.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 namespace="com.demo.dao.MeetingMapper"> <resultMap id="meetingMap" type="com.demo.entity.Meeting"> <id column="id" property="id"/> <result column="title" property="title"/> <result column="room_id" property="roomId"/> <result column="start_time" property="startTime"/> <result column="end_time" property="endTime"/> <result column="status" property="status"/> </resultMap> <select id="countConflict" resultType="int"> SELECT COUNT(*) FROM t_meeting WHERE room_id = #{roomId} AND status NOT IN (5, 6) AND start_time < #{endTime} AND end_time > #{startTime} </select> <insert id="insert" parameterType="com.demo.entity.Meeting" useGeneratedKeys="true" keyProperty="id"> 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> </mapper>XML 里<和>必须转义为<和>,这是新手最容易翻车的地方,不转义直接报 SQL 语法错误。useGeneratedKeys="true" keyProperty="id"让插入后能把自增主键回填到实体对象里,后面如果需要用这个 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"); PageInfo<Meeting> 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 里加了useUnicode=true&characterEncoding=utf8,最后在 web.xml 里配 CharacterEncodingFilter 强制请求和响应都用 UTF-8。
4.2 事务不生效
现象:Service 方法上加了 @Transactional,但插入失败后前面的操作没有回滚。原因通常是 Spring 配置里没有开启事务注解驱动,或者 Service 类没有被 Spring 扫描到。检查 spring-service.xml 里是否有<tx:annotation-driven transaction-manager="transactionManager"/>,以及包扫描路径是否覆盖了 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 = "GMT+8")保证序列化格式一致。
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 不存在、会议室已停用。这些边界情况才是真正让系统“能用”和“好用”的分界线。希望帮到你。
本文还有配套的精品资源,点击获取