拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于SSM的线上会议预约系统设计与并发冲突处理

简介一份基于SSM的线上会议管理系统毕业设计资源包面向计算机相关专业毕业生和需要快速搭建会议预约平台的开发者。系统采用Java MVC模式设计基于MySQL关系型数据库涵盖部门、会议室、公告、预约管理等功能并配有前端页面与后台管理模块能有效解决中小型公司会议预约和外出开会安排的信息化管理需求。资源包共包含2000个文件大小150.8MB主要文件类型有Java源码、JSP页面、JS脚本、CSS样式、HTML页面、XML配置文件以及SQL数据库脚本等其中JS处理前端交互JSP负责动态页面渲染SQL脚本可直接用于初始化数据库便于整体部署调试。资源同时提供LW论文文档和演示视频目录中明确列出系统实现与功能测试章节包括系统首页、后台管理、用户管理、预约管理、公告管理以及登录、注册、预约等测试用例可辅助理解开发思路和答辩准备。目前已有108人学习下载适合用于课程设计、毕业设计参考和在此基础上进行功能扩展。1. 线上会议预约的痛点与这套 SSM 系统的定位会议室资源一旦上了规模Excel 登记和口头预约很快会失控。员工想订会议室要翻群消息、问行政、等回复行政收到的又是“先占个时间”的模糊请求最后冲突只能人肉解决。这套基于 SSM 的线上会议管理系统要解决的正是这个高频低效问题Spring 管理业务对象与事务SpringMVC 负责路由和请求参数绑定MyBatis 把预约、公告、会议室数据落到 MySQL前端基于 Bootstrap 布局配合 summernote 让管理员可视化发布公告。它不是空壳展示页而是把部门、会议室、公告和预约管理串成一条完整业务链。适合做毕设选题、想快速熟悉 SSM MySQL 全栈的开发人员也适合把 Excel 维护升级为线上化的中小型公司。下面直接拆数据库、拆配置、拆核心预约逻辑。2. SSM MySQL 选型依据与数据库建模2.1 为什么小型会议系统适合 SSM 而不是 Spring Boot / 微服务这套系统采用 SSM在 Java Web 项目里属于非常成熟的组合。Spring 负责依赖注入和声明式事务SpringMVC 负责 URL 到 Controller 的映射MyBatis 把 SQL 写在 Mapper XML 里调优查询时不增加 Java 代码的编译成本。会议预约系统的表关系不复杂用户、部门、会议室、预约、公告五张核心表就能覆盖并发量也远没到微服务拆分的地步使用 SSM 单体应用加 MySQL 单库是够用的。对比 Spring Boot 来说SSM 的显式配置反而更适合学习者观察请求链路一个预约请求从 JSP 表单进入 DispatcherServlet再进入 Controller、Service、Mapper最后到 MySQL每一步都可以打断点。Spring Boot 自动装配虽然方便但初次接触时容易把很多细节当黑盒。从维护角度看这套系统的 Service 层如果保持接口设计后续迁移到 Spring Boot 只需要替换配置类和公共依赖业务代码几乎不用动。2.2 核心数据表设计部门、用户、会议室、预约、公告表结构设计看一张全貌就清楚表名用途关键字段t_dept部门id, dept_namet_user用户id, username, password, dept_id, rolet_meeting_room会议室id, room_name, capacity, location, statust_meeting预约记录id, meeting_name, room_id, user_id, begin_time, end_time, status, create_timet_announcement公告id, title, content, publisher_id, publish_time其中t_meeting表的room_id、begin_time、end_time是冲突校验的核心。常见的索引设计是room_id建普通索引(begin_time, end_time)建组合索引。查询“某会议室在某个时间段是否被占用”时通过begin_time #{endTime} AND end_time #{beginTime}判断区间重叠。这里有一个容易被忽略的点时间边界条件。如果会议 A 是 10:00-12:00新预约是 12:00-13:00正常不应该冲突所以条件要写成begin_time #{endTime} AND end_time #{beginTime}保证闭合区间不误判。外键关系上t_meeting引用t_user和t_meeting_room。会议系统数据量小保留外键能避免无用预约数据。如果后续要拆表或做分布式外键会影响写入性能那是后话。2.3 初始化数据库脚本示例拿到压缩包后第一步应该先跑数据库脚本。下面是一份可用的建表 SQL字段类型和注释都可以直接套用CREATE DATABASE IF NOT EXISTS meeting_system DEFAULT CHARACTER SET utf8mb4; USE meeting_system; CREATE TABLE t_dept ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, dept_id INT, role TINYINT DEFAULT 1 COMMENT 0-管理员 1-普通用户, FOREIGN KEY (dept_id) REFERENCES t_dept(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_meeting_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(30) NOT NULL, capacity INT NOT NULL, location VARCHAR(100), status TINYINT DEFAULT 1 COMMENT 1-可用 0-维护 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_meeting ( id INT PRIMARY KEY AUTO_INCREMENT, meeting_name VARCHAR(100) NOT NULL, room_id INT NOT NULL, user_id INT NOT NULL, begin_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待审批 1-通过 2-拒绝, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_room_id (room_id), KEY idx_time (begin_time, end_time), CONSTRAINT fk_room FOREIGN KEY (room_id) REFERENCES t_meeting_room(id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集使用utf8mb4而不是utf8原因很实际公告内容可能包含 emoji 或特殊符号utf8只能存三字节字符插入四字节字符会报错。password字段设计成VARCHAR(64)可以接住加盐后的 MD5 或 BCrypt 输出。role使用TINYINT保存角色标识不要用CHECK约束MySQL 8 之前对 CHECK 约束支持有限直接靠注释和 Java 枚举控制更省事。打开压缩包内的数据库文件时注意它可能包含演示数据。如果是为了二次开发建议把t_meeting清空再测试如果是直接演示功能保留演示数据可以更快看到首页效果。2.4 查询可用会议室MyBatis 动态 SQL 示例项目里查询可用会议室是最常用的操作。MyBatis 的 Mapper XML 可以这样写select idselectAvailableRooms resultTypemap SELECT r.id, r.room_name, r.capacity, r.location FROM t_meeting_room r WHERE r.status 1 AND r.id NOT IN ( SELECT m.room_id FROM t_meeting m WHERE m.status 1 AND m.begin_time lt; #{endTime} AND m.end_time gt; #{beginTime} ) ORDER BY r.capacity ASC /select这段 SQL 的判断逻辑是找出所有状态为“通过”的预约中与新预约时间段重叠的会议室 ID再在主查询里排除这些会议室。lt;和gt;是 XML 转义后的和写 XML 时必须用转义否则解析报错。#{beginTime}和#{endTime}会由 MyBatis 编译成 PreparedStatement 参数避免拼接 SQL 时的注入风险。如果会议室数量不多也可以先把全部会议室查出来在 Service 层用 Java 时间对象逐一判断但那样分页时会把分页逻辑搞复杂。SQL 层过滤是最直接的做法。另一个经验是给这条查询的room_id索引加上状态条件索引命中的效率会有明显提升。3. SSM 集成与会议预约核心模块实现3.1 Spring SpringMVC MyBatis 的配置要点SSM 项目拆包后第一件事是检查配置文件的路径扫描是否匹配。下面是一个常见的 spring-mvc.xml 配置context:component-scan base-packagecom.meeting.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean数据库相关的数据源和 SqlSessionFactory 通常放在独立的 db 配置文件中bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/meeting_system?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean mybatis:scan base-packagecom.meeting.mapper/注意 MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver老项目里经常看到com.mysql.jdbc.Driver如果本地是 MySQL 8必须换成前者。serverTimezoneAsia/Shanghai必须加上否则 JDBC 连接会报时区错误。Druid 数据源本身带连接池统计功能线上一旦出现连接数异常Druid 的监控页面可以直接看 SQL 耗时和慢查询这对于后期排查预约接口性能问题很有用。3.2 预约请求的处理流程Controller → Service → Mapper预约功能的接口路径可以这样规划接口路径方法作用/meeting/room/listGET获取当前可用会议室列表/meeting/reservePOST提交预约请求/meeting/cancelPOST取消已预约会议/meeting/myListGET查看当前用户预约记录Controller 负责接收参数、判断登录态业务逻辑放在 Service。一个典型的 Controller 方法是这样的Controller RequestMapping(/meeting) public class MeetingController { Autowired private MeetingService meetingService; PostMapping(/reserve) public String reserve(MeetingForm form, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/login; } Meeting m new Meeting(); m.setMeetingName(form.getMeetingName()); m.setRoomId(form.getRoomId()); m.setBeginTime(form.getBeginTime()); m.setEndTime(form.getEndTime()); m.setCreatorId(user.getId()); meetingService.reserve(m); return redirect:/meeting/myList; } }MeetingForm是前端表单对象的封装避免 Controller 每个方法都写一堆RequestParam。session.getAttribute(loginUser)先判断登录状态更优雅的做法是把这段放到拦截器里后面会讲。Service 层的冲突校验是核心Service Transactional public class MeetingServiceImpl implements MeetingService { Autowired private MeetingMapper meetingMapper; Override public void reserve(Meeting meeting) { int count meetingMapper.countConflict(meeting.getRoomId(), meeting.getBeginTime(), meeting.getEndTime()); if (count 0) { throw new BizException(该时间段会议室已被预约); } meetingMapper.insert(meeting); } }这里的Transactional很关键。冲突检查countConflict和插入insert必须处于同一个数据库事务中。如果在事务提交之前另一个请求也执行了countConflict可能拿到同样的 count0 结果然后双方都执行 insert最终出现两条重叠预约。要彻底解决这个问题需要用到下一节的锁机制。3.3 并发预约防冲突索引、锁与唯一性先明确一个边界给t_meeting.room_id begin_time end_time建唯一索引只能防住“完全相同的预约时间”防不住“时间区间重叠”。比如已有 10:00-12:00新请求 11:00-13:00唯一索引根本不会拦截因为它不是同一行记录。处理重叠区间的并发违约常见做法是使用SELECT ... FOR UPDATE锁住会议室所在行Transactional public void reserve(Meeting meeting) { // 对应 SQL: SELECT * FROM t_meeting_room WHERE id #{roomId} FOR UPDATE MeetingRoom room meetingRoomMapper.selectRoomForUpdate(meeting.getRoomId()); if (room null) { throw new BizException(会议室不存在); } int count meetingMapper.countConflict(meeting.getRoomId(), meeting.getBeginTime(), meeting.getEndTime()); if (count 0) { throw new BizException(该时间段会议室已被预约); } meetingMapper.insert(meeting); }FOR UPDATE表示当前事务对会议室这一行加排他锁另一个事务如果也尝试对该行执行FOR UPDATE会被阻塞直到前一个事务提交或回滚。这样就把冲突判断和插入操作串行化了。代价是当同一个会议室的并发预约量大时后面的请求需要排队。会议系统的并发量通常很低这种策略足够可靠。另一个方案是乐观锁在t_meeting_room表增加version字段预约前先UPDATE t_meeting_room SET version version 1 WHERE id ? AND version ?更新影响行数为 0 说明版本不对重试即可。乐观锁更适合读多写少的场景但要在重试逻辑上多写代码。对毕设和中小公司项目悲观锁更直观。4. Bootstrap 前端、summernote 公告与会话权限实战4.1 用 Bootstrap 搭建后台与预约页面前端从压缩包里的css/bootstrap.css、css/bootstrap.min.css就能看出是基于 Bootstrap 搭建的。页面布局通常是左侧菜单栏、右侧内容区Bootstrap 栅格系统刚好支持这种结构link href${pageContext.request.contextPath}/css/bootstrap.min.css relstylesheet link href${pageContext.request.contextPath}/css/summernote-bs3.css relstylesheet div classcontainer-fluid div classrow div classcol-sm-3 col-md-2 sidebar !-- 菜单栏 -- /div div classcol-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main !-- 预约表单、公告列表、会议室管理 -- /div /div /div${pageContext.request.contextPath}是 JSTL 取应用根路径的写法不加的话项目部署到非 ROOT 路径时样式会全部丢失。预约表单页面的提交推荐用 Ajax 方式页面不跳转、错误提示更友好$(#reserveForm).on(submit, function (e) { e.preventDefault(); var roomId $(#roomId).val(); var beginTime $(#beginTime).val(); var endTime $(#endTime).val(); if (!roomId || !beginTime || !endTime) { alert(请填写完整信息); return; } $.post(ctx /meeting/reserve, { roomId: roomId, beginTime: beginTime, endTime: endTime, meetingName: $(#meetingName).val() }, function (data) { if (data.code 0) { location.href ctx /meeting/myList; } else { alert(data.msg); } }); });ctx需要在 JSP 头部定义通常写成c:set varctx value${pageContext.request.contextPath}/。时间控件的值格式最好和后端约定为yyyy-MM-dd HH:mm:ss否则LocalDateTime.parse会失败。如果项目用java.util.Date还需要确认DateTimeFormat注解是否配置了对应的 pattern。4.2 summernote 富文本公告发布公告内容如果只是纯文本框体验会相当差。项目引入的summernote-bs2.css和summernote-bs3.css表明它使用了 summernote 编辑器。summernote 是 Bootstrap 生态中常用的富文本组件初始化方式$(function () { $(#summernote).summernote({ height: 200, lang: zh-CN, toolbar: [ [style, [bold, italic, underline]], [para, [ul, ol, paragraph]], [insert, [link, picture]], [view, [codeview]] ] }); });lang: zh-CN要生效必须在页面额外引入summernote-zh-CN.js语言包文件很多项目漏了这一步导致下拉菜单依旧英文。toolbar 里的codeview选项能让用户直接查看 HTML 源码对管理员发布格式化公告有帮助。summernote 默认插入图片时会把图片转成 Base64 嵌入content字段。公告数量少时问题不大但图片多时 MySQL 表会迅速膨胀。常见做法是关闭默认图片上传改成后端接口保存到服务器目录再把访问路径插入编辑器。如果只是毕设演示Base64 方案完全够用。安全方面要注意富文本提交到后端的是 HTML 片段如果直接c:out转义输出格式效果会全部丢失如果直接pageContext.response.getWriter().println()原生输出又可能被注入script。后端存储前应做过滤最简单的做法是把script标签整体清洗掉String safeContent content.replaceAll((?i)script[^]*.*?/script, );正则只作为基础防护。更严格的方案是引入 Jsoup 白名单过滤只允许p、span、img、a等标签。这套系统如果公告模块是给管理员用的风险相对可控但不要因此跳过这步。4.3 登录拦截器与权限控制预约和后台管理功能必须登录后才能访问最简单的控制方式是 HandlerInterceptor。拦截器代码public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(loginUser) ! null) { return true; } response.sendRedirect(request.getContextPath() /login); return false; } }在 spring-mvc.xml 中配置拦截范围mvc:interceptors mvc:interceptor mvc:mapping path/meeting/**/ mvc:exclude-mapping path/meeting/room/list/ mvc:exclude-mapping path/login/ bean classcom.meeting.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors/meeting/**会拦截所有以/meeting开头的路径。exclude-mapping用于放行不需要登录的接口比如首页展示可用会议室列表。管理员相关的用户管理、公告管理接口可以在 Controller 方法里再检查一次role字段如果接口数量多也可以再写一个角色拦截器统一判断 session 中的用户角色。密码存储是容易踩坑的点。很多 SSM 项目直接用明文或 MD5 存密码一旦数据库泄露全部账号都会被撞库。现在更推荐 BCrypt 或有盐哈希Spring Security 的BCryptPasswordEncoder可以直接用如果不引入额外依赖至少要对原始密码加盐后多次迭代哈希。5. 并发冲突复现、预约清理与 XSS 验证技巧5.1 用两个终端复现并发预约冲突可以打开两个 MySQL 客户端手动模拟并发事务验证之前提到的悲观锁效果。在会话 A 执行START TRANSACTION; SELECT * FROM t_meeting_room WHERE id 1 FOR UPDATE; -- 此时会话A持有会议室1的行锁 -- 手动插入一条预约并提交 INSERT INTO t_meeting (meeting_name, room_id, user_id, begin_time, end_time, status) VALUES (需求评审, 1, 2, 2026-06-10 10:00:00, 2026-06-10 11:00:00, 1); COMMIT;会话 B 同时执行START TRANSACTION; SELECT * FROM t_meeting_room WHERE id 1 FOR UPDATE; -- 会一直阻塞直到会话A提交阻塞现象正好说明行锁生效。如果不加FOR UPDATE两个事务同时读到没有冲突就会都执行 INSERT最终出现时间重叠的预约记录。这就解释了为什么单纯靠countConflict防并发不够。5.2 预约结束自动清理与回收长期运行的会议系统里预约记录会越积越多状态也容易停留在“已通过”。可以在 Spring 配置Scheduled定时任务每天凌晨扫描一次Scheduled(cron 0 0 2 * * ?) public void autoFinish() { meetingMapper.updateFinishedStatus(LocalDateTime.now()); }对应的 SQLUPDATE t_meeting SET status 3 WHERE status 1 AND end_time NOW();status 3可以定义为“已结束”这样会议室才真正释放。如果项目部署在多实例环境Scheduled会在每个实例上重复执行需要引入分布式锁或把定时任务集中到一个实例。单实例部署时这个方案没有问题。5.3 用 XSS 用例验证公告过滤发布公告时在标题和内容中插入scriptalert(1)/script然后以管理员身份浏览公告详情。如果弹出对话框说明后端没有过滤富文本内容如果没有弹出说明已经有过清洗或输出转义。用过滤正则时注意大小写不敏感(?i)是必须的前缀。验证完成后再检查一下预告功能是否仍然正常避免清理逻辑误伤正常的 HTML 格式。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门