十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java+Vue+SpringBoot构建高校讲座预约系统全解析

Java+Vue+SpringBoot构建高校讲座预约系统全解析 学生时代最头疼的事之一就是去听一场热门讲座。我记得那时候学术报告厅门口经常堵成一锅粥工作人员拿着纸质名单挨个核对有人提前一小时去占座有人临时来却发现座位已满。后来我帮学校信息化中心做了一个高校学习讲座预约系统用的就是标题里这套组合Java Vue SpringBoot。项目代号我习惯叫它“讲座通”整体做下来前后端加部署差不多三周时间。这篇文章我从头到尾拆一遍这套系统的设计与实现包括数据库怎么建、预约并发怎么处理、Vue端路由守卫怎么搞、打包部署有哪些坑以及我在实际开发过程中踩过的几个典型问题。不管你是准备做毕业设计还是想在公司内部搞一个活动预约平台这套思路都可以直接参考。1. 项目整体设计与技术选型1.1 讲座预约这个场景到底在解决什么先理清业务逻辑。高校讲座预约的核心不是“预约”本身而是资源分配和消息触达。在传统模式下讲座信息通过海报、QQ群转发来通知到场后人工签到。这种模式有三个痛点一是讲座信息分散学生不知道近期有什么讲座二是到场人数不可控热门讲座爆满、冷门讲座空场三是签到和学分认定依赖纸质记录统计麻烦。所以这个系统要解决的事情就是三件信息集中展示、预约行为在线化、后台数据可统计。学生可以看到所有待开始的讲座提前预约管理员可以发布讲座、设置名额、导出预约名单讲座开始前系统能推送给已预约用户提醒。整体就是个标准的信息管理加轻量级营销系统。1.2 为什么是SpringBoot Vue 而不是别的组合我在做这个项目之前其实还纠结过一套单体JSP方案。毕竟高校内部系统以前很多都是JSPServlet那种老架构。但后来放弃掉了主要原因是维护成本。JSP那套方案前后端代码混在一起讲师想改个首页轮播图都得找后端开发改代码。而前后端分离之后前端跑在Nginx上后端就是纯粹的API服务任何一端有改动只要接口契约不变就不会互相影响。这对学生团队或者少数人维护的项目来说非常友好。SpringBoot这块没太多好争的它已经是Java后端事实上的标准框架内嵌Tomcat、自动配置、不需要打WAR包丢容器一个java -jar直接跑起来。Vue这边我选的是Vue 2 Element UI。可能有人问2024年你还在用Vue 2我也知道Vue 3是趋势但这个系统是给学校内部用的稳定大于一切Element UI的生态在Vue 2下最成熟坑最少。如果你自己做新项目上Vue 3 Element Plus完全没问题但如果是维护已有系统Vue 2还能再战很多年。1.3 系统角色与功能模块划分我先花了一天梳理了系统角色和功能画了两张角色用例图这一步千万别省。最终梳理出三种角色学生用户前端注册登录、浏览讲座列表、查看讲座详情、预约讲座、取消预约、个人中心查看我的预约、讲座留言。管理员后台讲座发布与管理、分类管理、预约记录查看与导出、用户管理、留言审核、统计数据。超级管理员管理员账号管理主要是分配不同权限的管理账号比如学术部账号只能管讲座团委账号只能看数据。功能模块上就清晰了前端有首页轮播图最新讲座、讲座列表支持分类筛选、关键字搜索、讲座详情、预约页、个人中心后端有用户模块、讲座模块、预约模块、留言模块、文件上传模块、数据统计模块。整个系统的功能需求就是围绕“发布—查看—预约—管理”这条主线展开的别在前期把功能想太复杂先跑通主流程再逐步加东西。2. 核心数据模型设计一口气建好这6张表2.1 数据库建模的思路数据库设计是整个项目的地基。我见过太多人一上来就写代码写着写着发现缺字段、缺关联回头改表结构牵一发动全身。我建议先花半天时间把表建好后面会轻松很多。梳理下来这个项目至少需要6张核心表user用户表lecture讲座信息表lecture_category讲座分类表reservation预约记录表lecture_comment留言评论表admin管理员表其中lecture_category和lecture是一对多关系user和lecture是典型的多对多关系通过reservation这个中间表关联。一个学生可以预约多场讲座一场讲座可以被多个学生预约这就是最典型的选课场景只不过把课程换成了讲座。2.2 核心表结构逐张拆解先看用户表。我考虑到有些系统要对接学校统一身份认证所以在表里预留了student_no字段暂时不强制唯一后续如果要对接教务处数据可以直接用这个字段关联。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 用户名, password varchar(128) NOT NULL COMMENT 加密后密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, student_no varchar(50) DEFAULT NULL COMMENT 学号, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) DEFAULT 0 COMMENT 角色0-学生 1-管理员, status tinyint(4) DEFAULT 1 COMMENT 状态1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码这里我用了BCrypt加密这是Spring Security自带的加密器但它不是每次加密结果都一样你猜为什么BCrypt加密有个特点每次加密同一个密码得到的hash值都不同但是校验的时候都能够通过。因为它把随机盐值直接拼在了hash里。这比MD5安全得多因为同一个密码不会产生同样的密文字典攻击很难奏效。我专门试过同一个“admin123”连续加密两次结果完全不同校验却都能通过这个细节面试很容易被问到。讲座信息表是核心中的核心字段比较多我直接贴简化版本CREATE TABLE lecture ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 讲座标题, speaker varchar(50) DEFAULT NULL COMMENT 主讲人, speaker_intro varchar(500) DEFAULT NULL COMMENT 主讲人简介, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, location varchar(100) DEFAULT NULL COMMENT 讲座地点, start_time datetime DEFAULT NULL COMMENT 开始时间, end_time datetime DEFAULT NULL COMMENT 结束时间, total_seats int(11) DEFAULT 100 COMMENT 总名额, reserved_count int(11) DEFAULT 0 COMMENT 已预约人数, cover varchar(255) DEFAULT NULL COMMENT 封面图, content text COMMENT 讲座详情, status tinyint(4) DEFAULT 0 COMMENT 状态0-未开始 1-进行中 2-已结束, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;total_seats和reserved_count这两个字段非常关键。很多人设计时容易只做一个total_seats预约人数用count(*)去实时统计在小数据量下没毛病但一旦数据量大起来每次查询都要count一次性能堪忧。所以我在讲座表里直接维护了一个reserved_count每成功预约一次就1取消预约就-1。这种空间换时间的思路在这个场景下非常实用。需要注意的一点是如果系统对数据一致性要求极高比如涉及收费或严肃的考勤登记那必须用专用计数表不能用这种冗余字段。预约记录表要特别注意唯一约束的问题CREATE TABLE reservation ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, lecture_id bigint(20) NOT NULL COMMENT 讲座ID, reserve_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 预约时间, check_status tinyint(4) DEFAULT 0 COMMENT 签到状态0-未签到 1-已签到, cancel_flag tinyint(4) DEFAULT 0 COMMENT 是否取消0-否 1-是, PRIMARY KEY (id), UNIQUE KEY uk_user_lecture (user_id, lecture_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_user_lecture这个唯一约束在代码层面就是user_id和lecture_id的双重校验。没有这个约束就有可能出现用户反复点击预约按钮产生了多条预约记录从而绕过“每人只能约一次”的业务规则。数据库层面的约束是最后一道防线必须加上。其余的留言表、分类表、管理员表都比较常规留言表无非就是lecture_id关联讲座、user_id关联用户、content存内容、status做审核状态管理员表我单独建了一张没有和用户表混在一起因为管理员字段比如最后登录IP、权限等级和学生字段差别较大拆开更清晰。2.3 一个关于时间字段的小提醒讲座时间的存储我是用的是datetime类型但是在后端Java代码里对应的字段用的是LocalDateTime而不是java.util.Date。原因很简单LocalDateTime操作更安全不容易出现时区问题而且配合Jackson序列化非常顺畅。你只要做好格式化配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样就避免前端收到2024-06-01T10:30:00这种“T”字母的ISO格式显示出来是2024-06-01 10:30:00干净利落。3. 后端核心模块实现与避坑细节3.1 项目初始化与依赖管理我建SpringBoot项目用的是Spring Initializr版本选了Spring Boot 2.7.x没有选3.x。这里有个很重要的原因Spring Boot 3要求JDK 17而且包名从javax.servlet改成了jakarta.servlet很多老资料和第三方组件都要适配。用2.7.x的话JDK 8或者11都能跑网上搜到的教程大部分也都能直接照着用。我们给学校做的系统没有特殊需求2.7.18这个版本是最稳妥的选择。顺带一提热词里提到“springboot版本太高”的问题大概率就是Spring Boot 3的jakarta包名变更导致的。pom.xml里核心依赖就这六个spring-boot-starter-webWeb基础mybatis-plus-boot-starterMyBatis-Plus简化CRUDmysql-connector-javaMySQL驱动lombok省去写getter/setterjjwtJWT工具包做tokenhutool-all工具库包含各种好用的Java工具方法3.2 登录认证与JWT Token的完整实现登录认证我用的方案是JWT 拦截器。相比Session方案JWT天然适合前后端分离后端不需要维护会话状态用户登录后拿到一个token每次请求带上后端验签就行。这在分布式环境下非常友好因为不需要会话复制。JWT的结构分三段Header头部、Payload载荷、Signature签名。头尾两端都是Base64编码中间那段载荷放着用户id、用户名和过期时间。最后用密钥对整个前两段做HMAC SHA-256签名防止内容被篡改。实际应用中我会在用户登录成功后生成一个有效期12小时的tokenString token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 12 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();前端拿到token后存储到localStorage在请求拦截器里统一加上请求头// axios拦截器 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })后端对应的拦截器会去解析这个token解析时注意捕捉异常。token过期、被篡改、签名不匹配都会抛出异常。我的处理方式是写一个AuthInterceptor从请求头取出token然后调用JWT工具类解析解析失败直接返回401状态码前端统一跳回登录页。这里分享一个我踩过的坑JWT在生成时不要把密码或手机号放入Payload因为Payload只是Base64编码谁都能解码出来看。我只放userId和role其他敏感信息一律不发。3.3 预约并发控制——如何避免“超卖”问题预约系统最经典的坑是并发超卖。想象一个场景讲座只有100个座位第99个人预约成功后第100和第101个人同时提交预约请求没有控制的话系统可能让两个人都预约成功导致实际到场人数超过名额。我用的方案是双层校验核心就是“先查后改”加“条件更新”。先查一下讲座状态和当前预约人数是否已满如果满了就直接拒绝若没满执行更新时在SQL层面再做一次条件判断UPDATE lecture SET reserved_count reserved_count 1 WHERE id #{lectureId} AND reserved_count total_seats这条SQL能保证只有当前预约数小于总名额时更新才会成功。受影响行数如果是1说明抢座成功如果是0说明在这一瞬间名额已经满了就拒绝请求。同时用户维度的防重复预约靠的是数据库的唯一约束。在插入预约记录前先查一次插入时数据库再拦一遍。这个逻辑写在线程同步、悲观锁之前已经能挡住绝大多数并发问题。如果你想做得更严谨还可以加Redis分布式锁。在大学校园这个用户规模下MySQL本身的行锁加上唯一约束已经非常稳了我实测过用JMeter并发100个请求最后预约记录和讲座计数完全一致没有超卖。3.4 统一返回类与全局异常处理前后端分离项目接口返回格式必须统一。我定义了一个Result类包含code、message、data三个字段。成功返回code200业务失败返回code4xx未登录返回code401。没有统一返回格式的时候前端每个接口都要判断返回结构代码写得极其痛苦。统一之后前端封装一个返回值工具函数拿到code不为200的直接弹出错误提示非常清爽。全局异常处理也很关键。我写了一个RestControllerAdvice切面专门捕获业务异常、参数校验异常和系统异常。其中一个不起眼但很实用的细节是用RestControllerAdvice做异常拦截时一定要在ExceptionHandler里区分异常类型不能一网打尽否则会给前端返回一个巨大的堆栈信息既不安全也不友好。参数校验这里我用的是Validated注解加实体类上的校验注解比如NotBlank、Email专门处理那些必须传的参数。一开始我也是自己在代码里一个个判断写多了实在烦后来全部改成注解校验代码量减半而且格式很统一。4. 前端Vue项目实现从零搭到能跑4.1 初始化Vue项目与目录结构规划我用Vue CLI 5创建的项目基于Vue 2.6。创建命令没什么好说的就一条vue create lecture-front。重点在于创建之后目录结构一定要按照模块划分而不是一把梭把组件全丢到views里。我的目录结构是这样的src/ api/ # 接口请求封装按模块拆文件 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 store/ # Vuex状态管理 views/ # 页面组件 admin/ # 后台管理页面 lecture/ # 讲座列表、详情、预约页面 user/ # 用户登录、注册、个人中心 home/ # 首页 utils/ # 工具函数axios封装就在这里api目录下按业务模块拆文件每个文件里导出一个函数对应一个后端接口比如api/lecture.js里就是getLectureList、getLectureDetail、reserveLecture、cancelReservation等。页面组件里不直接出现请求地址这样后期改接口路径只需要改一个地方。4.2 Axios封装与拦截器axios封装是Vue项目里必做的一件事。我创建一个utils/request.js负责统一配置基础URL、超时时间、请求拦截和响应处理。几个关键点基础URL配置为baseURL: /api开发环境下通过Vue CLI的代理转发请求到后端生产环境由Nginx做同样的代理这样前端代码完全不需要感知后端地址。请求拦截器统一添加token。响应拦截器统一判断状态码。code200直接返回response.data.data前端调用的时候少一层嵌套code401说明token失效清除本地登录态并跳转登录页code500弹错误提示。超时时间我设置的是10秒有些接口涉及文件上传可能需要更长的时间再单独传入配置覆盖默认值。这个小细节如果不在封装时考虑进去后期遇到大图片上传就会很尴尬。4.3 路由配置与登录守卫路由方面我用的是Vue Router的路由懒加载每个页面组件都是一个动态导入大大减少首屏加载时间。前端路由分两块普通用户的路由和后台管理的路由。后台管理的所有组件都放在views/admin/目录下路由路径以/admin开头。对应的路由守卫逻辑是router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) return } if (to.path.startsWith(/admin) role ! 1) { next(/) return } next() })有两个细节值得注意第一判断登录状态不能只靠localStorage里有没有token理论上token可能已过期。但前端再精确的判断都已经没有意义后端会拦截前端只是做体验优化。第二后台管理需要双重判断既要有token还要角色是管理员否则直接跳回首页。这属于前端路由级权限控制真正安全的后端接口校验是必不可少的。4.4 讲座列表与预约页面的实现细节讲座列表页是整个系统访问量最大的页面我做了分类筛选和搜索。分类和搜索都挂到URL的query参数上比如/lecture/list?keywordAIcategoryId3这样刷新页面后状态还在也方便分享链接给同学。列表数据通过watch监听路由变化后重新请求。这里有个小坑直接监听$route对象时如果只变了query参数组件实例是复用的不会触发created钩子必须在watch里加上handler。我用的方案是watch: { $route.query: { handler() { this.fetchList() }, immediate: true } }预约按钮的逻辑核心是前置判断。如果用户没登录点击预约跳转到登录页如果讲座已满按钮置灰显示“已满员”如果该用户已经预约过按钮显示“已预约”并且点击可以跳转到取消预约确认框。卡时间点这件事在预约按钮上也要处理。系统当前时间如果已经晚于讲座开始时间就不允许预约了。这个判断在后端也做了防止有人绕过前端直接调接口预约已开始或已结束的讲座。两边都做校验前端方便用户快速感知后端保证数据安全。4.5 后台管理页面的表格与弹窗处理后台管理页面几乎全是表格。我基于Element UI的el-table来搭每张表配一个搜索区域、一个新增/编辑弹窗、一个删除确认操作。管理讲座时新增和编辑共用一个弹窗组件用editType字段区分是新增还是编辑。弹窗里用到el-form表单校验规则绑定在rules属性上比如讲座标题必填、开始时间必填、总名额必须大于0。上传封面图用的是Element UI的el-upload组件action指向后端的/api/upload接口。后端处理上传的关键是限制文件大小和类型防止有人上传恶意文件。我限制的是图片类型jpeg/png/jpg/webp单张不超过5MB。5. 部署上线从打包到Nginx配置5.1 后端打包与启动后端打包需要先确保测试通过然后执行mvn clean package -DskipTests。跳过测试是为了打包速度快但前提是你确信代码没问题。打包出来的jar包通常在target/目录下大小约60-80MB。启动之前要确认好生产环境的数据库连接配置和Redis配置。我习惯用Spring的多环境配置spring: profiles: active: prodapplication-prod.yml里放生产环境的数据库地址、端口、密钥等信息。这样开发环境、测试环境、生产环境的配置完全隔离切换环境只需要改一个active。后台启动用nohup java -jar lecture-admin.jar app.log 21 这个命令前台直接执行会占用终端关闭终端服务就挂了。生产环境更正式的做法是用systemd托管但对于学校内部系统nohup基本够用。5.2 前端打包与Nginx部署前端打包执行npm run build产物在dist/目录。这个时候有个常见问题打包后布局异常、图片访问404。
返回列表