☰
Spring Boot校园讲座管理系统:全流程设计与实现
2026/10/6 9:46:38 网站建设 项目流程

办一场校园讲座,最麻烦的往往不是准备内容,而是把"通知发布—在线报名—现场签到—事后统计"这条链路管明白。我在帮学校信息中心做内部工具时体会特别深:讲座信息散落在各院系群和Excel表格里,学生要么错过报名,要么到场后排长队签到,老师事后统计到场人数还得手工翻表格。后来我基于Spring Boot把校园讲座管理系统完整搭了一遍,所有流程收口到一个后台,讲座发布、审核、报名、签到、评价、数据统计全部打通,源码也整理成了可运行的完整工程。如果你正在找Java课程设计、毕业设计参考,或者想搞明白一个Spring Boot项目从零到能部署的完整套路,这篇博文应该能帮你少走不少弯路。

1. 项目整体设计与技术选型

1.1 核心需求拆解:一场讲座背后的完整业务链条

表面上这是一个CRUD项目,但真动手前我习惯先把业务链路拆清楚。一场讲座从发起到结束,大约要经历七个环节:讲座发起、内容审核、对外发布、学生报名、现场开讲、到场签到、事后评价与统计。每个环节对应不同角色的诉求:主办方关心"讲座有没有被审核通过、报名人数有没有满";学生关心"最近有什么讲座、还剩多少名额";管理员关心"哪个分类的讲座办得多、到场率怎么样"。

这个系统设计的第一原则是:面向流程,而不是面向表单。换句话说,不能只做一张讲座表的增删改查,而要让每条讲座数据都有清晰的状态。比如一个讲座可以是草稿、待审核、已发布、已结束,这样前端才能根据状态去展示不同的按钮,后端也才能在报名时判断"这个讲座现在能不能报名"。很多新手一做管理系统就陷入"写死一堆接口"的误区,把精力全花在页面字段上,忽略了数据流转,结果项目看上去功能很多,实际上经不起追问。

所以我在需求阶段定了几个更具体的目标:支持多角色登录(管理员、教师、学生),讲座信息维护有审核环节,报名要能防重复和防超员,签到要在报名基础上完成核销,统计要能按分类和时间维度聚合。这些目标组合起来,项目才真正有了"管理系统"的骨架。

1.2 技术栈选型:为什么是Spring Boot + MyBatis Plus而不是其他组合

技术选型这块,我直接选了Spring Boot作为主框架,没犹豫。原因是校园讲座管理系统这种体量的Java Web项目,用Spring Boot最合适。它天然内置了Tomcat、Starter依赖、自动配置,开发时不用像传统SSH那样花大量时间拼XML配置,Maven中央仓库拉依赖也简单。更现实的一点是,这个项目在课程设计和毕业设计里很常见,Spring Boot体系的资料多、坑少,后续扩展和答辩提问都更容易应对。

持久层我用了MyBatis Plus。有人会问为什么不用Spring Data JPA?我承认JPA在单表操作上更"省事",但校园管理类系统通常要写一些复杂的统计SQL,比如按月份聚合报名人数、按分类统计讲座场次,MyBatis Plus在SQL控制上更直接。它的BaseMapper和LambdaQueryWrapper能处理掉大约七成的简单CRUD,ComplexSQL自己写XML也不复杂,既有省事的封装,又保留了手写SQL的能力。

认证方案选了Spring Security + JWT。简单说,Spring Security负责拦截请求和角色鉴权,JWT负责在无状态接口之间传递登录身份。校园讲座系统是典型的前后端分离场景:前端Vue或者简单页面调用后端接口,如果沿用传统Session在各个服务器间同步,麻烦且不适合横向扩展。用JWT后,用户登录一次拿一个Token,后续每次请求带上,后端验签就行。Redis在这里我做了可选配置:如果讲座列表经常被访问,可以用Redis做热点缓存;不加也不影响核心业务,初期部署更简单。

1.3 功能模块规划:一个管理系统该有的完整闭环

模块划分直接决定开发节奏。我把系统拆成七个功能模块,每个模块之间保持低耦合:

  • 用户与权限模块:登录、注册,管理员维护用户信息,角色区分管理员、教师、学生
  • 讲座分类模块:分类的增删改查,讲座创建时必选分类
  • 讲座信息模块:讲座的草稿、提交审核、审核通过/驳回、讲座详情维护
  • 报名管理模块:学生在线报名、取消报名、防重复、防超员
  • 签到管理模块:主办方确认到场,记录签到状态
  • 评价反馈模块:讲座结束后学生打分和留言
  • 数据统计模块:讲座场次、报名趋势、分类占比、到场率核心指标

这里想多说一句:报名和签到我刻意拆成两个模块,但数据上都挂在"报名记录"这一条链上。签到本质是报名资格的核销,不需要单独建表,在报名记录上加一个到场状态字段即可,既省表又直观。很多实战项目都喜欢多建表,结果查询时要join一堆,维护成本陡增。能用字段表达的状态,就别用表去表达。

2. 核心细节解析与实操要点

2.1 数据模型设计:从字段到表关系的取舍

数据库设计是这类系统最见基本功的地方。我梳理了一下,核心就五张表:用户表sys_user、讲座分类表lecture_category、讲座表lecture_info、报名记录表lecture_registration、评价表lecture_feedback。

sys_user表字段不多:id、username、password、real_name、role、phone、email、create_time。密码这里必须存BCrypt加密后的值,千万别用明文。角色字段用字符串"ADMIN"、"TEACHER"、"STUDENT"表示,简单可控,不用为三个角色建复杂的RBAC表。

lecture_info是重点表。核心字段是title、speaker、category_id、start_time、location、capacity、enrolled_count、status、audit_status、publisher_id。我在这里把status和audit_status分开设计:status表示业务状态(草稿0/已发布1/已结束2),audit_status表示审核状态(未提交0/待审核1/通过2/驳回3)。为什么要分开?因为"审核通过"和"讲座已结束"描述的不是同一个维度,混在一个字段里会导致后续查询状态时逻辑混乱。这是我在真实项目里踩过坑之后总结出来的:能用两个独立字段表达的状态,不要合并成一个数字假装节省字段。

lecture_registration表是整个系统防重和签到的关键。字段就是id、lecture_id、user_id、register_time、attend_status。这里有一个非常重要的细节:要给(lecture_id, user_id)加联合唯一约束。这样即使前端并发点了两次报名,数据库层也能兜住,不会出现一条重复报名记录。签到用attend_status区分:0未签到,1已签到。

lecture_category表就简单了:id、name、sort,最多再加个备注。评价表lecture_feedback记录lecture_id、user_id、rating、comment,同样加唯一约束,保证一个学生只能对同一场讲座评价一次。

2.2 权限体系与接口设计规范

权限设计如果不做控制,学生调一个管理员接口就能删讲座,项目直接崩盘。我按照角色划分了三层权限:

角色能做什么典型接口
管理员审核讲座、管理用户、查看全部统计、管理分类/api/admin/**
教师发布讲座、编辑自己的讲座、查看自己讲座报名名单、发起签到/api/lectures/**(部分)
学生浏览讲座、报名/取消、签到、提交评价/api/student/**

统一接口前缀的方式让我在后端配置Spring Security时非常轻松:/api/auth/**路径放行(登录注册),/api/admin/**要求ADMIN角色,其余接口需要登录即可。Spring Security的URL匹配顺序要注意:更具体的规则写在前面,比如/api/admin/lectures/**要先于/api/lectures/**匹配,否则角色校验会被提前放行或者被错误拦截。

统一返回结构是我做这个项目时给自己定的规范。所有接口一律返回{ code: 200, message: "success", data: ... }这样的格式,前端只需要封装一个request.js处理code即可。如果返回结构不统一,前端处理每个接口都要单独判断,混乱程度会直线上升。这个规范应该在写第一个接口前就定好,写完后统一用枚举类定义错误码,不要随手写数字。

2.3 讲座状态机与报名防重:最容易翻车的两个业务细节

整个项目里真正的业务难点不在CRUD,而在两个状态控制逻辑上。

第一个是讲座状态机。讲座从创建到可报名要经历:教师保存草稿(status=0)→ 提交审核(audit_status=1)→ 管理员审核通过(audit_status=2, status=1)→ 讲座结束(status=2)。驳回时回到草稿态,教师修改后可重新提交。这个状态流转用if-else去写极其容易漏条件,我的经验是先把状态转换图在草稿纸上画出来,再用枚举判断。报名接口必须检查audit_status == 2 && status == 1,二者缺一不可。你想想,一个被驳回的讲座如果没检查审核状态就开放报名,学生报了名主讲人还不知情,那真是事故了。

第二个是并发报名防重。校园讲座热门场次经常一两分钟就爆满,学生同时点击报名,后端如果是"查询人数→判断是否超员→插入记录"的三步走,极有可能出现超卖。我采用的做法是两步:先查讲座状态和当前报名数,如果未满,执行一条受保护的更新语句UPDATE lecture_info SET enrolled_count = enrolled_count + 1 WHERE id = ? AND enrolled_count < capacity,只有当受影响行数为1时,才向lecture_registration插入报名记录。这相当于用数据库层面的条件更新来充当乐观锁,避免同时进来的请求都认为"还有名额"。

容量修改也有一个小坑:讲座发布后如果主办方要调低容量上限,必须校验新容量大于当前已报名人数,否则会出现"数据库里报名人数大于容量"的脏数据。这个校验写在后端service层,接口层面统一拒绝非法修改。

3. 核心环节实现与部署记录

3.1 环境准备与项目初始化

我实际搭建时的环境配置如下:JDK 1.8(当然用11也没问题)、Maven 3.6+、MySQL 5.7/8.0、IDEA 2022及以上、Navicat或DataGrip作为数据库客户端。Redis这版项目是可选项,不开不影响核心流程。

创建数据库时注意字符集,推荐直接用utf8mb4,避免将来要存emoji或者中文昵称时乱码。SQL脚本里预置管理员账号:admin / 123456(密码已用BCrypt加密),以及几个测试学生账号。首次导入时我遇到过一个问题:如果数据库是MySQL 8.0,驱动要写成com.mysql.cj.jdbc.Driver,并且连接串必须带上serverTimezone=Asia/Shanghai,否则会报时区异常。

关键的application.yml配置大致长这样:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/lecture_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

注意map-underscore-to-camel-case这个配置。数据库字段是start_time,Java属性是startTime,不开启下划线转驼峰,MyBatis Plus查询结果就映射不上,查出来的对象全是null。这是非常经典的入门卡点。

3.2 登录认证模块:JWT的完整落地

认证模块我拆成了三个部分:JwtUtil工具类、JwtAuthenticationFilter过滤器和SecurityConfig配置。JwtUtil负责生成和解析Token,核心代码如下:

public class JwtUtil { private static final String SECRET = "your-secret-key-change-in-production"; private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

Token过期时间我设置为7天。校园讲座系统不是银行核心系统,学生一周内反复重新登录会非常烦躁;但如果做的是交易系统或者后台管理系统,这个过期时间就必须缩短到半小时到两小时之间。安全策略永远要结合业务场景定,不是越长越安全,也不是越短越好。

JwtAuthenticationFilter继承OncePerRequestFilter,核心逻辑就是取Header里的Authorization、去掉Bearer前缀、解析Token、把userId和role塞进SecurityContext。这里有一个我栽过的坑:过滤器里如果解析失败,不能直接抛异常,要捕获异常后清理SecurityContext,否则后续接口会拿到脏认证信息。

SecurityConfig配置做了这三件事:放行登录注册接口、拦截其他接口、添加JWT过滤器。

http.csrf().disable() .cors().and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**", "/api/lectures/list").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

/api/lectures/list放行是因为讲座列表页不需要登录就能看到,但报名必须要登录。cors()这里要提前写好CORS配置类,开发时前端Vue默认端口往往和后端不一样,不配置跨域,页面请求全部失败。

3.3 讲座发布与审核的实现

讲座发布的入口是教师端的一个表单,包含标题、主讲人、分类、时间、地点、容量、简介等字段。保存时区分两种操作:存草稿和提交审核。

@PostMapping("/api/lectures") public Result createLecture(@RequestBody LectureSaveDTO dto, @AuthenticationPrincipal UserPrincipal user) { Lecture lecture = new Lecture(); BeanUtils.copyProperties(dto, lecture); lecture.setPublisherId(user.getId()); lecture.setStatus(0); // 草稿 lecture.setAuditStatus(0); // 未提交 lecture.setEnrolledCount(0); lectureService.save(lecture); return Result.success(); }

提交审核时,把audit_status改成1,管理员在/api/admin/lectures/audit接口处理。审核通过后audit_status=2、status=1,讲座对外可见;驳回时audit_status=3,并且必须写驳回原因。我在写驳回原因时发现一个体验细节:如果只是改一个状态值,学生端看到"讲座不可用"却不知道为什么,主办方也很困惑。所以在lecture_info表加了一个audit_remark字段,驳回原因存进去,教师端打开列表就能看到,修改后重新提交。这个字段虽小,但审核流程的可用性立刻就上来了。

3.4 报名、签到与统计功能的实现

报名接口是整个系统最典型的业务代码,我直接贴核心逻辑:

@Transactional public Result register(Long lectureId, Long userId) { Lecture lecture = lectureService.getById(lectureId); if (lecture == null || lecture.getStatus() != 1 || lecture.getAuditStatus() != 2) { return Result.fail("讲座不存在或未开放报名"); } int rows = lectureMapper.increaseEnrolledCount(lectureId); if (rows == 0) { return Result.fail("报名人数已满"); } try { LectureRegistration registration = new LectureRegistration(); registration.setLectureId(lectureId); registration.setUserId(userId); registration.setAttendStatus(0); registrationService.save(registration); } catch (DuplicateKeyException e) { throw new BusinessException("您已经报名过该讲座"); } return Result.success(); }

increaseEnrolledCount对应的SQL是UPDATE lecture_info SET enrolled_count = enrolled_count + 1 WHERE id = ? AND enrolled_count < capacity,这一步保证并发安全。我特意在方法上加了@Transactional,避免人数增加了但报名记录插入失败导致的数据不一致。

签到功能实现起来相对简单:主办方在报名名单页面勾选"签到",后端校验该学生确实报名过,然后把attend_status置为1。如果扩展到扫码签到,实质也是把这个操作封装成一个带时效的Token接口。统计功能我用了三条SQL赚足大头:

-- 按分类统计讲座场次 SELECT category_id, COUNT(*) FROM lecture_info WHERE status = 2 GROUP BY category_id; -- 按月统计报名人数趋势 SELECT DATE_FORMAT(register_time, '%Y-%m') AS month, COUNT(*) FROM lecture_registration GROUP BY month; -- 讲座到场率 SELECT l.id, l.title, COUNT(r.id) AS registered_count, SUM(CASE WHEN r.attend_status = 1 THEN 1 ELSE 0 END) AS attended_count FROM lecture_info l LEFT JOIN lecture_registration r ON l.id = r.lecture_id WHERE l.status = 2 GROUP BY l.id;

聚合查询用MyBatis Plus的QueryWrapper也能写,但这种多表LEFT JOIN统计我还是建议写到XML里或者用注解SQL,语义更清楚,也方便后续加索引优化。

4. 常见问题排查与经验分享

4.1 开发中踩过的六个坑,逐个说明

项目做完后我最想保留的是一份问题速查表,因为这些坑基本覆盖了Spring Boot新手最容易翻车的几个位置。

问题现象可能的根因解决方案
前端请求接口报跨域错误后端未启用CORS,或配置了allowCredentials(true)但allowedOrigins("*")冲突明确指定允许的来源域名,不要用通配符+credentials
登录后调用接口仍返回401过滤器放行路径顺序不对,或Token过期先确认路径匹配顺序,再检查JWT过期时间
查询出的实体字段全是null没开启map-underscore-to-camel-case,属性名对不上在application.yml加配置,或为字段加@TableField
后端返回时间比数据库少了8小时服务器、数据库、连接串时区不一致连接串显式加serverTimezone=Asia/Shanghai
上传讲座封面图片失败Spring Boot默认上传大小限制1MB配置spring.servlet.multipart.max-file-size
同一条数据逻辑删除后,再次插入报唯一键冲突逻辑删除字段与联合唯一索引冲突把逻辑删除字段加入业务唯一判断,或在删除时物理删除旧的报名记录

第一个跨域坑困扰过我很久。CORS配置一旦出现allowedOrigins("*")配合allowCredentials(true),浏览器会直接拦截响应。正确做法是写成准确的Origin列表,或者在开发环境单独配置一个允许的本地端口。

第二个坑是JWT。我调试时发现,Token明明生成成功了,请求还是被拦截。后来发现是SecurityConfig里我把jwtFilter加到了UsernamePasswordAuthenticationFilter后面,导致认证信息还没设置,权限判断就开始执行。过滤器的挂载顺序一定要在UsernamePasswordAuthenticationFilter之前。

第三个坑写出来可能有人觉得笨,但真的非常常见。MyBatis Plus默认开启驼峰映射,但前提是数据库字段是下划线风格。如果你的表字段本来就叫startTime而没有下划线,那映射是正常的;一旦混合命名,一查一个不吱声。建议建表时全部用下划线,Java属性全部用驼峰,这是最省心的组合。

4.2 安全与性能自查清单

虽然是校内系统,但安全底线不能丢。我给自己列了一个自查清单,分享几个最关键的:

第一,密码必须BCrypt加密。新增用户时用BCryptPasswordEncoder.encode(),登录校验时用matches()。这个类Spring Security已经内置,不要自己写MD5加盐逻辑,BCrypt每次加密会生成随机盐,安全性比MD5高一个量级。

第二,管理员接口的路径权限要配置到位。/api/admin/**必须要求ADMIN角色,不能只靠前端隐藏按钮。前端隐藏只是体验优化,后端拦截才是安全底线。测试方法是:用一个学生Token直接请求管理员接口,看是否返回403,如果放行了,说明配置有问题。

第三,列表查询必须分页。MyBatis Plus的Page对象用起来很简单,但很多人在写讲座列表时图省事直接list()查全表,数据量一上来页面就卡死。分页的同时在lecture_info表的start_time和category_id上建索引,报名表在(lecture_id, user_id)上建联合索引即可。如果访问量比较大,讲座详情这类热点接口可以考虑用Redis包一层,Key设计为lecture:detail:{id},设置一小时过期时间,在这个体量的系统里足以应付绝大多数压力。

第四,接口层尽量少的打印明文敏感数据。登录接口的请求日志不要完整打印密码,审计日志保留操作人、操作时间和操作动作就够了,这也是真实项目里常见的合规要求。

4.3 后续扩展方向,按实际优先级排序

如果一个学期后学校提出要扩展,我会按这个优先级来排:首先做消息通知,报名成功后用邮件或企业微信机器人推给学生,省去主办方逐个通知;然后做扫码签到,生成一场讲座专属的二维码,学生扫码后校验报名记录并签到,把现场排队的体验彻底解决;接着可以做讲座回放视频挂载,和讲座详情页打通;最后有条件的话,把统计页面升级成可视化看板,用ECharts把分类占比和到场率画成图表,汇报时直接投屏。按这个顺序扩展,每一期都有独立价值,不会做成过度设计的玩具。

这套系统的技术栈依然是主流通用方案。Spring Boot负责后台接口,MyBatis Plus处理数据访问,JWT无状态认证让前后端分离自然流畅。如果你也想动手做类似的项目,我的建议是先从数据模型开始看,把lecture_info的状态字段和lecture_registration的唯一约束理解透,再往上叠加接口和页面。数据结构稳了,功能扩展就是加接口的事;数据结构乱了,后面每一步都在还债。

我个人在实际搭建中体会最深的一点是:把"报名"和"签到"用同一张报名记录表承接,用attend_status区分阶段,这个设计在编码阶段看不出优越性,但到了统计到场率和排查重复数据时,省了不知道多少功夫。很多设计上的取舍,要到运维和排查阶段才知道当初的选择到底香不香。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询