news 2026/9/18 16:20:02

SpringBoot会议管理系统:MySQL建模、冲突校验与权限控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot会议管理系统:MySQL建模、冲突校验与权限控制

简介:这是一份面向计算机相关专业毕业生与Java Web开发初学者的毕业设计论文文档,以「基于SpringBoot的会议管理系统」为选题,可用于毕业设计参考、论文写作范例学习及课程项目选题借鉴。资源为1个doc格式文档,压缩包约5.12MB,内容围绕系统概述、需求分析、数据库设计、前后端编码实现与测试等章节展开,行文完整、结构规范。目前已有142人学习下载,适合需要搭建同类管理系统或撰写论文的读者参考。文档完整呈现了员工管理、公告管理、会议室管理、会议资料管理、会议投票与意见收集等模块的设计思路,数据库层面涉及用户表、会议表、公告表、会议室表等实体字段规划,技术选型涵盖Java、SpringBoot、MySQL与HTML前端,并包含单元测试、集成测试与系统测试的验证过程。读者可据此了解一个Java Web管理系统的需求梳理、库表设计与开发测试全流程,也可借鉴其论文目录组织与章节写法,为自身选题提供可复用的框架与思路。

1. 一次会议排期引发的返工:这套系统到底在解决什么

公司规模到了几十人往上,会议室排期靠群里喊话和 Excel 共享,问题就暴露得特别快。有人约了 3 楼大会议室,另一个部门同时在纸上写了同一时段,两边人到门口才发现撞车;会议资料散在个人网盘,会后想找回某版决议得翻聊天记录;投票表决只能靠举手表决再人工统计。这些场景的共同点不是"缺个表单",而是缺一张能约束时间资源、能把资料和结论沉淀下来的数据层。

基于 SpringBoot 的会议管理系统要处理的正是这类问题。它把员工、会议室、会议、会议资料、投票、意见收集这几类对象放进 MySQL,通过 SpringBoot 提供的 REST 接口做增删改查,管理员维护基础数据和公告,员工端浏览会议室、查看资料、参与投票、提交意见。系统边界很清楚:不做视频会议,不做即时通讯,只做会议组织过程中的信息流转与状态跟踪。

适合谁读?拿这个题目做计算机毕业设计的同学,能直接对照论文里的功能模块抄出可运行的后端结构;需要给中小团队搭内部预约工具的在职开发,也能从里面的冲突校验 SQL 和权限切分方式里挑走能用的部分。下面从工程骨架讲到数据库建模,再落到冲突校验和接口排错,最后补测试与验收演示的实操细节。

2. SpringBoot 工程初始化与会议管理领域的数据建模

很多同学在第一步就卡住,不是不会写代码,而是被版本组合搞崩了。搞清楚选型再动手,能省掉后面一半的调试时间。

2.1 版本组合与工程骨架生成

当前 Spring Initializr 网页默认给的是 SpringBoot 3.x,而 3.x 强制要求 JDK 17 起步。学校机房或者自己电脑上装的还是 JDK 1.8,导入项目后启动直接报Unsupported class file major version,这就是热搜里"版本太高想回退到 1.8"的典型现场。毕设场景稳妥的组合是 SpringBoot 2.7.18 + JDK 1.8 + Maven,理由是 2.7 是 2.x 最后一个长期维护版本,网上资料最多,MyBatis 生态也最成熟。

生成方式有两种。网页端在 Initializr 上把 Boot 版本手动切到 2.7.x,Java 选 8,勾选 Spring Web、MyBatis Framework、MySQL Driver、Lombok。想要命令行版本可以直接拼:

# 生成 SpringBoot 2.7.18 骨架,JDK 8,含 Web/MyBatis/MySQL curl https://start.spring.io/starter.zip \ -d type=maven-project \ -d language=java \ -d bootVersion=2.7.18 \ -d javaVersion=8 \ -d groupId=com.example \ -d artifactId=meeting \ -d name=meeting \ -d dependencies=web,mybatis,mysql,lombok \ -o meeting.zip unzip meeting.zip -d meeting && cd meeting

参数说明:bootVersion决定 Spring 生态的主版本,写成 2.7.18 就能绕过 3.x 的 JDK 门槛;javaVersion=8保证maven.compiler.source落在 1.8;dependencies里 web 提供内嵌 Tomcat 和@RestController,mybatis 提供 Mapper 扫描,mysql 只是引入驱动,实际连接串还得自己写。骨架跑起来后用mvn spring-boot:run验证,看到 Tomcat 在 8080 端口启动即算通过——这一步能过,后面的坑基本都是业务坑。

提示:如果 IDE 里创建项目时下拉框根本没有 2.7.x 选项,说明本地 Initializr 插件版本太新,直接用上面的 curl 或改pom.xml里的<parent>版本号更省事。

2.2 员工、会议室、会议三张核心表

论文里写了"数据库 ER 图设计"和"数据库表设计",但落到代码上,真正撑住业务的是三张表加两张从表。设计原则是先定主键类型,再定约束。下面是我在毕设里推荐的建表方式:

-- 员工表:区分管理员与普通员工,用 role 字段而非两张表 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT 'MD5加盐后的密文', real_name VARCHAR(32) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '0员工 1管理员', dept VARCHAR(64) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会议室表:容量和位置决定排期时的筛选条件 CREATE TABLE meeting_room ( id BIGINT NOT NULL AUTO_INCREMENT, room_name VARCHAR(64) NOT NULL, location VARCHAR(128) DEFAULT NULL, capacity INT NOT NULL DEFAULT 10, status TINYINT NOT NULL DEFAULT 1 COMMENT '1可用 0停用', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会议表:start_time/end_time 是冲突校验的核心字段 CREATE TABLE meeting ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL, room_id BIGINT NOT NULL, creator_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待开 1进行 2结束', PRIMARY KEY (id), KEY idx_room_time (room_id, start_time, end_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键点在最后一行索引idx_room_time。冲突校验的查询条件是"同一会议室 + 时间区间重叠",把room_id放最左、时间字段跟随,能让这个查询走覆盖索引,几十条会议数据看不出差别,但答辩演示时如果准备了上千条模拟数据,有没有这个索引就是秒回和转圈的区别。sys_user用单个role字段而不是拆成管理员表和员工表,是因为两边的公共属性(账号、姓名、部门)完全一致,拆表只会让登录接口多写一个分支。

2.3 MyBatis 映射与 JSON 序列化的边界

SpringBoot 论文里提到"实现了 SQL 语句的分离,这就是所谓的 MyBatis 框架",这句话落到工程上就是resources/mapper/*.xml这个目录。实体类和表字段的映射有三条常见路线,选错了后面改字段会很难受:

方案配置方式适用场景坑点
驼峰自动映射mybatis.configuration.map-underscore-to-camel-case=true字段命名规范统一real_name能映射realName,但自定义别名失效
resultMap手写XML 里逐字段声明有联表查询、字段别名字段多了冗长,改表要同步改两处
注解@Results写在 Mapper 接口上单表简单查询复杂动态 SQL 不如 XML 直观

我一般用第一种打底,联表查询单独写resultMap。配置文件里补上:

# application.yml 关键片段 spring: datasource: url: jdbc:mysql://localhost:3306/meeting_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

serverTimezone=Asia/Shanghai这行建议都写上,否则 MySQL 8 驱动在部分环境下会把DATETIME读成前一天,表现为"预约的是 5 月 1 日,查出来是 4 月 30 日",这种时间偏移排查起来非常费时。前端通过 JSON 拿数据时,LocalDateTime默认序列化成2024-05-01T09:00:00这种带 T 的格式,如果 Vue 端直接用字符串截取会出错,稳妥做法是在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),或全局配置ObjectMapper的日期格式。

3. 会议室冲突校验、资料归档与投票模块的落地实现

三张表建好之后,真正决定系统好不好用的是业务规则。会议室不能重复占用、投票不能刷票、资料不能越权下载,这三条规则写歪一条,演示时就会被追问。

3.1 时间段交集 SQL 与并发预约防重

判断两个时间段是否重叠,本质是四种情况取反:新会议的开始时间早于已有会议的结束时间,且新会议的结束时间晚于已有会议的开始时间。用 SQL 表达就是:

-- 校验目标会议室在 [newStart, newEnd) 区间是否已被占用 SELECT COUNT(1) FROM meeting WHERE room_id = #{roomId} AND status != 2 -- 已结束的会议不参与冲突计算 AND start_time < #{newEnd} -- 已有会议早于新会议结束前开始 AND end_time > #{newStart}; -- 已有会议晚于新会议开始后结束

参数roomId是用户选中的会议室,newStart/newEnd是前端传回的时间串。这段 SQL 在 Service 层调用,返回值大于 0 就抛出业务异常提示"该时段已被占用"。注意这里用的是严格小于和严格大于,也就是允许"上一场 10:00 结束、下一场 10:00 开始"这种背靠背排期,如果业务上要求留出 15 分钟清场,把条件改成start_time < DATE_ADD(#{newEnd}, INTERVAL 15 MINUTE)即可。

单机演示环境这样写够了,但只要有两个请求同时进来,先查后插之间仍然存在竞态窗口。要彻底堵住,可以在meeting表上加一条唯一约束或改用悲观锁:

// Service 层:把"查重 + 插入"放进同一个事务,并对会议室行加锁 @Transactional(rollbackFor = Exception.class) public void createMeeting(MeetingDTO dto) { // 对会议室记录加行锁,串行化同一会议室的预约请求 roomMapper.selectByIdForUpdate(dto.getRoomId()); int conflict = meetingMapper.countConflict( dto.getRoomId(), dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { throw new BizException("该会议室在所选时段已被占用"); } meetingMapper.insert(dto.toEntity()); }

selectByIdForUpdate对应的 SQL 末尾要加FOR UPDATE,这样两个并发请求会排队进入,第二个拿到的锁一定是第一个事务提交之后的。代价是同一会议室的高并发场景下响应变慢,不过会议预约这种低频操作完全能接受。@Transactional里指定rollbackFor = Exception.class是因为默认只对运行时异常回滚,业务里抛的自定义BizException若不是继承RuntimeException,事务不会回滚,数据就脏了。

3.2 会议投票的防重与事务边界

投票模块的核心诉求有两个:一是同一员工对同一议题只能投一次,二是票数统计要准。防重最省事的办法不是写代码判断,而是在表上加唯一索引让数据库兜底:

-- 投票记录表:user_id + topic_id 唯一,从存储层杜绝重复投票 CREATE TABLE vote_record ( id BIGINT NOT NULL AUTO_INCREMENT, topic_id BIGINT NOT NULL COMMENT '关联会议投票议题', user_id BIGINT NOT NULL, option_id BIGINT NOT NULL COMMENT '所选选项', vote_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_topic_user (topic_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

有了uk_topic_user,重复投票在insert时就会抛DuplicateKeyException。业务层捕获它转成友好提示即可,省掉一次"先查有没有投过"的查询,也彻底避免并发下判断失效。统计票数时按option_id分组:

-- 统计某议题各选项得票,含未选项展示需要前端自行补零 SELECT o.id AS optionId, o.option_text AS optionText, COUNT(v.id) AS voteCount FROM vote_option o LEFT JOIN vote_record v ON v.option_id = o.id AND v.topic_id = o.topic_id WHERE o.topic_id = #{topicId} GROUP BY o.id, o.option_text ORDER BY o.id;

LEFT JOIN而不是INNER JOIN的原因很实际:如果某个选项一票都没有,内连接会让它整行消失,前端图表里就少一根柱子,看起来像 bug。用左连接保证每个选项都返回,COUNT(v.id)因为只数非空值,天然得到 0。参数topicId对应投票议题,是一张单独的vote_topic表,属于meeting的子表,设计时别把议题字段直接塞进会议表,否则一个会议想搞两个独立投票就得改表结构。

3.3 会议资料上传与落盘路径策略

会议资料管理在论文里是一整个模块,实现上要回答两个问题:文件存哪、谁能下。存哪这块,毕设阶段最常见也最稳妥的做法是存本地磁盘,数据库只存相对路径:

@PostMapping("/material/upload") public Result<?> upload(@RequestParam("file") MultipartFile file, @RequestParam("meetingId") Long meetingId) throws IOException { // 按会议 ID 分目录,避免单目录文件数过多 String dir = uploadRoot + File.separator + meetingId; File dirFile = new File(dir); if (!dirFile.exists() && !dirFile.mkdirs()) { throw new BizException("创建上传目录失败"); } // UUID 重命名,规避中文名和同名覆盖问题 String origin = file.getOriginalFilename(); String suffix = origin.substring(origin.lastIndexOf(".")); String savedName = UUID.randomUUID() + suffix; file.transferTo(new File(dirFile, savedName)); MeetingMaterial m = new MeetingMaterial(); m.setMeetingId(meetingId); m.setOriginName(origin); // 展示给用户看的原名 m.setStorePath(meetingId + "/" + savedName); // 实际落盘相对路径 materialMapper.insert(m); return Result.ok(); }

uploadRoot来自配置项,别硬编码,否则换台机器演示就得改代码。suffix从原名里截取而不是用MultipartFile.getContentType()推断,因为浏览器给的 MIME 类型并不可靠。最关键的一点在下载接口:一定要在服务端校验"当前登录用户是否有权访问该会议的资料",不能只靠前端隐藏链接,否则把 ID 一改就能拿到别人的会议文件。

4. 员工端与管理端权限切分及接口联调排错

论文把功能分成管理员和员工两套,实际操作里这两套共用登录、共用一张用户表,靠角色字段分流。分流写不对,要么员工能删会议室,要么管理员进不去后台。

4.1 拦截器与角色字段的配合

推荐用 Spring 的HandlerInterceptor在进入 Controller 之前做校验,比在每个方法里写 if 判断干净得多:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { // 1. 未登录直接拦截 Object user = req.getSession().getAttribute("loginUser"); if (user == null) { resp.setStatus(401); return false; } // 2. 管理端接口要求 role == 1 String uri = req.getRequestURI(); if (uri.startsWith("/admin/")) { SysUser u = (SysUser) user; if (u.getRole() == null || u.getRole() != 1) { resp.setStatus(403); return false; } } return true; } }

注册的时候要注意排除登录、注册和静态资源路径,否则用户连登录页都打不开。/admin/前缀是约定,所有只有管理员能调的接口统一挂这个前缀,拦截器只判断前缀就行,不用维护一张长长的白名单。这种写法比用注解加反射的方案简单,毕设规模完全够用;如果后面接口多了想升级,再换 Spring Security 也不迟。

会话方案上,毕设用HttpSession最省事,登录成功把用户对象塞进 session,前端每次请求带JSESSIONIDcookie。唯一要注意的是前后端分离部署时跨域 cookie 的携带问题,如果浏览器控制台报SameSite相关警告,就在拦截器返回时补resp.setHeader("Access-Control-Allow-Credentials", "true"),同时前端请求配置里打开withCredentials

4.2 接口报错的定位顺序

联调阶段 90% 的问题集中在四类返回码上,按下面顺序排查最快:

现象常见原因定位动作
400 Bad Request前端 JSON 字段名和后端 DTO 不匹配打印请求体,核对@RequestBody字段
401 Unauthorizedsession 过期或未携带 cookie看浏览器 Application 里有没有 JSESSIONID
403 Forbidden角色不符合/admin/前缀要求查当前登录账号的role
500 Internal Server Error空指针、SQL 语法、类型转换看控制台完整堆栈,重点看 Caused by

排 500 的时候有个提速技巧:在application.yml里打开 MyBatis 的 SQL 日志,mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl,这样控制台会把实际执行的 SQL 和参数打出来,一眼就能看出是条件没传进去还是字段拼错。参数传null导致的空指针,用@RequestParam(required = false)或者给 DTO 字段设默认值都能挡住,但治本的办法还是在 Service 层入口做一次非空校验。

另外一个高频坑是日期参数。前端日期选择器给的是"2024-05-01"字符串,后端 DTO 用LocalDate接没问题,但如果是LocalDateTime又没传时分秒,会直接抛DateTimeParseException变成 400。统一约定前端传"yyyy-MM-dd HH:mm:ss"并在 DTO 字段上加@DateTimeFormat就能对齐。

5. 测试用例分层与答辩现场演示的两个技巧

论文第六章写了"测试实例"和"测试结论",实际答辩时老师最爱问的就是"你怎么保证冲突校验是对的"。与其背结论,不如现场跑两条边界用例。冲突校验的边界值至少覆盖五种:完全重叠、部分重叠、首尾相接、完全不相交、跨天。前三种走接口能直接验证 SQL 条件写对了没有,后两种最容易漏,尤其是跨天——如果只按日期比较而不比较时间,一场 23:00 到次日 01:00 的会议就会误判成不冲突。

单元测试用 JUnit 5 加@SpringBootTest即可,重点测 Service 层:

@Test @Transactional // 测试完自动回滚,不污染演示库 void shouldRejectOverlapMeeting() { MeetingDTO a = buildDto(1L, "2024-05-01 09:00:00", "2024-05-01 10:00:00"); meetingService.createMeeting(a); // 第一场正常 MeetingDTO b = buildDto(1L, "2024-05-01 09:30:00", "2024-05-01 11:00:00"); assertThrows(BizException.class, () -> meetingService.createMeeting(b)); // 重叠必须被拒 }

@Transactional加在测试方法上让每次执行后自动回滚,避免反复跑测试把演示数据搅乱。断言用assertThrows而不是捕获异常后手动fail(),前者失败信息更直观,输出里会直接告诉你期望的异常没抛出来。

第二个技巧是关于演示数据。答辩现场最好预先灌入一批"有故事"的数据:同一会议室相邻时段的两场会议、一个已经结束的会议、一个还有票没投完的投票、一份带中文名的会议资料。这样演示冲突校验、状态流转、投票统计、文件下载四个功能时都有现成的靶子,不用现场手忙脚乱地新建。灌数据的脚本写成一个data.sql放在src/test/resources下,配合spring.sql.init.mode=always只在演示环境启用,生产配置里关掉即可。答辩前把后端打个 jar,用java -jar meeting-0.0.1-SNAPSHOT.jar --spring.profiles.active=demo一条命令启动,比现场开 IDE 等 Maven 编译稳得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 16:18:19

四颗工业级核心芯片的系统级选型与落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 16:17:49

OpenMed 快速入门:从零搭建本地医疗 NER 与 PII 去标识化环境

OpenMed 快速入门&#xff1a;从零搭建本地医疗 NER 与 PII 去标识化环境 【免费下载链接】openmed Local-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud…

作者头像 李华
网站建设 2026/9/18 16:15:34

人工智能核心技术有哪些?机器学习、知识图谱与语音交互实战

简介&#xff1a;这份PDF文档围绕「人工智能的核心技术」展开&#xff0c;依据《人工智能标准化白皮书&#xff08;2018&#xff09;》的框架&#xff0c;面向人工智能入门学习者、备考人员及需要梳理知识体系的从业者&#xff0c;解答AI核心技术包含哪些内容这一问题。文档以机…

作者头像 李华
网站建设 2026/9/18 16:14:37

Django图片服务器完整指南:从上传到访问的链路设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 16:14:06

Linux WiFi设备驱动开发实战:从SDIO到数据包的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华