简介:面向高校社团信息化管理及课程设计、毕业设计场景,这份压缩包提供了基于Spring Boot、Vue与MySQL构建的前后端分离社团管理系统完整源代码和数据库脚本。文件总数约2000个,以1364个MD说明文档、409个JS前端脚本为主体,另有Java源码、XML/JSON配置与SQL数据库文件,包体约45.69MB,安装部署与二次开发均可直接参照。系统功能覆盖社团信息管理、活动发布、入团申请审核与费用管理,管理员可维护社团资料和财务记录,学生能在线申请入团、及时查看活动动态。包内目录层级清晰,便于快速定位前后端模块、配置文件和数据库初始化脚本,适合需要获取可运行项目原型、梳理主流技术栈项目结构或用于课程答辩参考的学习者。目前已有115人学习下载。
1. 学生社团管理系统:一份能直接跑的 Spring Boot + Vue + MySQL 全栈源码
“学生社团管理系统”这类项目在毕设和课设里出现频率极高,但真正能一次跑通、还带完整数据库的源码包并不多。这份资源是我拆过比较顺手的:后端 Spring Boot 提供 REST API,前端 Vue 负责页面交互,MySQL 存全部业务数据,从社团创建、成员报名、活动发布到审批管理,整套闭环都有对应代码,SQL 文件导入就能出数据。相比网上那些只给 Controller 和 Mapper 片段、不给全量建表脚本的资源,这份起码不用你自己猜表结构。适合三类人:正在做毕设需要完整项目兜底的学生、想学全栈联调流程的初学者、以及接外包时想快速交付社团类业务的一线开发。接下来我把启动步骤、核心接口怎么改、踩过的坑都拆开讲。
2. 技术栈与系统设计:为什么要这么搭,数据模型又是怎么定的
2.1 从技术选型看这套组合的生产力
Spring Boot + Vue + MySQL 的组合能成为主流,不是因为它“新”,而是因为它把前后端职责切得干净。后端 Spring Boot 负责把 Controller、Service、Mapper 三层拆开,事务、依赖注入、数据源配置都是约定优于配置,不用再写一堆 XML。前端 Vue 用组件化思路组织页面,配合 Vue Router 做路由跳转,Vuex/Pinia 管登录态和用户信息,开发体验比直接操作 DOM 好得多。
在社团管理这个场景里,数据规模不大,并发也不高,MySQL 足够胜任。它的事务支持能保证社团报名操作的一致性,比如报名成功的同时要扣减活动名额,这两步要么都成功要么都失败,没有事务就容易出现名额超卖。相比把数据塞进 Redis 或者在内存里模拟,MySQL 是最容易被审阅者认可的设计,也最方便后续扩展,毕竟社团管理系统最终还要对接年级、学院、指导老师等业务维度,关系型数据库更合适。
2.2 核心功能模块与角色权限
从实际使用角度看,这个系统至少要有三类角色。系统管理员负责创建社团、审核社团成立申请、管理全局用户;社团负责人负责自己社团内的成员审核、活动发布和公告维护;普通学生可以浏览社团列表、提交加入申请、查看已加入的社团和活动报名记录。
权限设计建议走“菜单 + 接口”双重控制。前端根据角色动态渲染路由,比如普通学生不显示“成员审核”入口;后端则通过拦截器或注解校验接口权限,比如修改社团信息时必须校验当前用户是否是该社团的负责人。这套源码里我建议重点看后端鉴权部分,它能帮你避免前端只是隐藏了按钮、接口还能被人直接调用的问题。
2.3 数据库设计:核心表结构和关键字段
这套系统的数据库脚本我拆解下来,核心表大概有七八张,我列一下业务上绕不开的几张。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id, username, password, role_id, real_name, student_no |
| club | 社团表 | id, club_name, description, president_id, max_member, status |
| club_member | 社团成员表 | id, club_id, user_id, join_time, role_type |
| activity | 活动表 | id, club_id, activity_name, start_time, end_time, max_people |
| activity_signup | 活动报名表 | id, activity_id, user_id, signup_time, status |
| notice | 公告表 | id, club_id, title, content, create_time |
先说 user 和 club 的关联,这里不能用外键强行绑定,因为用户加入多个社团是常态,社团表的 president_id 只是冗余了一个负责人标识,真正的关系要通过 club_member 这张中间表维护。我在实际项目里吃过亏,一开始只给 club 表加了 member_count 字段,报名通过后执行 update club set member_count = member_count + 1,后来迁移数据库时发现数据对不上,查了半天。
我的建议是把 member_count 当作缓存字段,真正的数据以 club_member 表的行数统计为准,定时任务或统计接口再回填。activity 表里的 status 字段建议用 int 而不是 varchar,比如 0 表示未开始、1 进行中、2 已结束、3 已取消,用 varchar 存“进行中”这种中文值,后续做时间范围查询非常别扭。
数据库初始化脚本里有两个细节容易被忽略。一是所有表都要设置 utf8mb4 字符集,否则插入“🎉”“🔥”这类 emoji 时会报错,学生系统里活动公告很容易带表情。二是 user 表密码字段长度至少要 60,因为 BCryptPasswordEncoder 加密后的字符串有 60 个字符,varchar(20) 会被截断。这两点我在多个项目里都踩过,先放在前面提醒。
3. 环境准备与项目启动:从 JDK 配置到前后端跑通
3.1 环境清单和版本匹配关系
不要一上来就装最新版。Spring Boot 2.x 对应 JDK 8 或 11,Vue 2 对应 Node 14 或 16,MySQL 用 5.7 或者 8.0 都能跑,但连接驱动写法略有差异。我习惯的版本组合如下。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | Spring Boot 2.6.x 以下用 1.8 最稳 |
| Maven | 3.6 以上 | 管理后端依赖 |
| Node.js | 14.x 或 16.x | 过新版本的 Node 跑 Vue 2 项目会报 OpenSSL 错误 |
| MySQL | 5.7 或 8.0 | 8.0 需要配置时区参数 |
如果你用的是 Node 17 以上版本,跑 npm run dev 时大概率会碰到Error: error:0308010C:digital envelope routines::unsupported,这是 Node 新版本对 OpenSSL 算法限制导致的。解决方案有两种,一是在 package.json 里把开发脚本改成set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve,二是直接把 Node 降到 16 版本,我比较推荐后者,省得每次启动都要带环境变量。
3.2 后端启动:先改配置再建库
后端的核心配置文件是 src/main/resources/application.yml,启动前先确认数据源指向。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/club_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none show-sql: true这里有几个参数要解释。serverTimezone 必须写成 Asia/Shanghai,不写的话 MySQL 8.0 的连接会直接报时区错误;useSSL=false 关闭 SSL 校验,因为本地开发环境根本没有证书;ddl-auto 我建议保持 none,让程序只负责查询,表结构完全由 SQL 脚本控制,这样多人协作时不会因为实体类字段被自动同步而出现脏数据。
确认配置后,用 IDEA 或命令行启动主类。
mvn clean install -DskipTests mvn spring-boot:run第一次执行会下载大量依赖,如果网速慢可以换阿里云镜像。启动日志里看到Tomcat started on port(s): 8080就说明后端起来了。这一步常见的翻车点是端口被占用,我后面会在排错章节专门讲。
3.3 前端启动与代理配置
前端项目解压后,先看有没有 node_modules 目录,没有就先安装依赖。
npm install npm run serve如果 npm install 特别慢,可以临时切换淘宝镜像源,执行npm config set registry https://registry.npmmirror.com,装完再改回去。启动前端前,先检查 vue.config.js 里的 devServer 配置,开发环境下要让前端把 /api 开头的请求转发到后端 8080 端口。
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };这个代理配置非常重要,如果不配置,浏览器直接请求 http://localhost:8080 会触发跨域,后端即使加了 CORS 配置也会被浏览器拦截真实请求,所以我习惯在开发环境直接走代理,生产环境再交给 Nginx 转发。pathRewrite 的意思是前端请求 /api/user/login,后端实际收到的是 /user/login,这样后端控制层就不用额外加 /api 前缀。
3.4 数据库导入与初始账号
找到资源包里的 club_system.sql 文件,用 Navicat 或者命令行导入。
mysql -uroot -p123456 < club_system.sql导入后必须验证三件事。第一,确认数据库名称与后端 application.yml 里的库名一致;第二,确认 user 表里有一条角色为管理员的数据,否则你登录不了系统;第三,确认 club_member 表有真实数据,不然列表页空白,你会误以为代码有 Bug。初始账号通常在 SQL 文件或 README 里有注释,默认一般是 admin/admin123,具体以脚本里的插入语句为准。我见过太多人卡在登录这一步,最后发现是密码字段存的是明文,而后端校验用的是 BCrypt,这属于数据库和数据源不匹配的问题。
前端页面打开后,先在登录页用管理员账号走一遍流程,确认能跳转到后台首页。如果页面一直在转圈,打开浏览器 F12 看 Network 面板,找到请求失败的接口,大概率是后端服务没起来,或者代理没生效。
4. 核心业务改造:报名审批、成员管理、活动发布怎么动代码
4.1 社团报名与审批状态机
社团报名的核心不是“插入一条记录”,而是状态的流转。学生提交报名后,记录状态是 0(待审核),社团负责人审核通过变成 1(已加入),拒绝变成 2(已驳回)。这个状态机在后端 Service 层就应该写好,不要在前端多个组件里各写一套逻辑,否则改一次流程要改三四个地方。看一下报名接口的写法。
@Override @Transactional public Result signUpClub(Long userId, Long clubId) { ClubMember existing = clubMemberMapper.selectByUserIdAndClubId(userId, clubId); if (existing != null) { return Result.error("你已加入该社团,请勿重复申请"); } Club club = clubMapper.selectById(clubId); if (club.getStatus() != 1) { return Result.error("该社团未通过审核,暂不可加入"); } ClubMember member = new ClubMember(); member.setUserId(userId); member.setClubId(clubId); member.setJoinTime(new Date()); member.setStatus(0); clubMemberMapper.insert(member); return Result.success("申请已提交,等待审核"); }这段代码的核心是第一个判断,查询是否已存在相同用户和社团的记录。这个检查必须放在 Service 层,因为前端按钮可以禁用,但接口没法靠前端约束。@Transactional 注解保证了 insert 出错时事务回滚。club.getStatus() 的校验容易被漏掉,社团如果被管理员停用,学生就不应该再提交申请。
审批通过的方法,本质上就是更新状态字段。
@Transactional public Result approveMember(Long memberId) { ClubMember member = clubMemberMapper.selectById(memberId); if (member == null) return Result.error("记录不存在"); if (member.getStatus() != 0) return Result.error("该申请已被处理过"); member.setStatus(1); clubMemberMapper.updateById(member); return Result.success("已通过审核"); }这里判断 status != 0 是为了防止二次审核,不然负责人手快点了两次“通过”,状态会被重复更新,虽然业务影响不大,但审计记录里会留下脏数据。
4.2 成员管理的移除与转让
成员管理不只是增删改查,有个边界情况容易翻车:社团负责人移除所有成员后,自己也被踢了,导致社团变成“无主状态”。所以移除成员时要判断被移除的人是不是社团的 president_id,如果是,就要求先转让负责人给其他成员,否则直接报错。
另一个常见的需求是成员加入时间的展示。club_member 表里的 join_time 在接口返回时最好格式化成 yyyy-MM-dd,不然前端拿到一串时间戳还得自己处理。我一般习惯在后端直接格式化,避免前端重复写转换工具。
4.3 活动报名:人数限制和冲突检测
活动报名比社团报名多一个限制条件:活动名额有限。如果活动设置了 max_people,报名时要先查当前报名人数。
SELECT COUNT(*) FROM activity_signup WHERE activity_id = #{activityId} AND status = 1前端在做报名按钮时,活动列表接口返回的 signup_count 可能已经是缓存值,如果同时多个学生抢同一个活动最后几个名额,后端的判空逻辑不够严谨就会出现超卖。我建议在后端报名接口开启事务,并且在 activity 表增加一个 version 字段做乐观锁,更新时带上WHERE version = #{version},如果更新影响行数为 0 就说明名额已被别人占走。
活动时间冲突检查也是一个必要逻辑。学生报名活动前,检查该学生在同一时间段内是否已经有其他报名记录,避免同时参加两个时间重叠的活动。这个查询稍微复杂一点,需要比较活动的 start_time 和 end_time 是否存在交集。
SELECT COUNT(*) FROM activity a LEFT JOIN activity_signup s ON a.id = s.activity_id WHERE s.user_id = #{userId} AND s.status = 1 AND a.start_time < #{endTime} AND a.end_time > #{startTime}这段 SQL 的核心是活动时间段相交的判断条件,两条活动记录只有当一条的开始时间小于另一条的结束时间、并且一条的结束时间大于另一条的开始时间,才说明时间有重叠。我自己第一次写的时候用了大于小于的反向条件,查出来的结果完全不对,后来画了一条时间轴才理清楚。
4.4 活动发布的表单校验
活动发布的 Controller 层参数校验也很关键,不能只靠前端校验。活动名称为空、活动开始时间晚于结束时间、活动人数上限超过社团最大容量,这些都应该在后端校验。
@PostMapping("/publish") public Result publishActivity(@RequestBody Activity activity) { if (StringUtils.isBlank(activity.getActivityName())) { return Result.error("活动名称不能为空"); } if (activity.getStartTime().after(activity.getEndTime())) { return Result.error("活动开始时间不能晚于结束时间"); } if (activity.getMaxPeople() > 200) { return Result.error("活动人数上限不能超过200人"); } return activityService.publish(activity); }这套资源里用的校验方式可能不是这个风格,但思路是通用的。如果你在改代码时发现前端传过来的是表单数据而不是 JSON,Controller 里的 @RequestBody 就要换成普通参数接收,别到时候前端报 400 错误,你还在后端排查半天。
前端活动列表页,建议直接用 Vue Router 的详情页传参,通过查询参数携带活动 id,不要在列表页重复编写活动详情的数据查询逻辑。
this.$router.push({ path: '/activity/detail', query: { id: row.id } })详情页通过 this.$route.query.id 拿到参数,再调后端接口获取完整信息。这种写法的好处是列表和详情页职责分离,列表页只管加载列表数据,详情页只负责展示单条数据。
5. 部署与排错:跨域、端口冲突、MySQL 版本差异这些坑
5.1 端口被占用导致后端启动失败
现象:后端启动时报Port 8080 was already in use,或者启动日志里什么错误都不报,但访问接口始终无响应。原因:系统里另一个进程占了 8080 端口,开发环境下常见的是 IDEA 的另一个项目或某些后台服务。解决:先查端口占用情况。
netstat -ano | findstr 8080Windows 下拿到 PID 后,在任务管理器里找到对应进程结束掉。如果你希望一劳永逸,改后端端口更省事,把 application.yml 里的 server.port 改成 8081,同时把前端 vue.config.js 的 proxy target 也改成 8081。这类端口问题我写过很多次,强烈建议第一次启动就统一端口规划,比如后端 8080、前端 8081,能少踩不少坑。
5.2 MySQL 连接报错:时区、驱动、密码校验
现象:启动时报localtime is not recognized或者Public Key Retrieval is not allowed。原因:MySQL 8.0 默认时区和加密规则与 5.7 不同,驱动配置里没指定 serverTimezone,或者连接参数里少了 allowPublicKeyRetrieval。解决:更新连接串,加上时区参数和允许公钥检索的参数。
spring: datasource: url: jdbc:mysql://localhost:3306/club_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true如果你用的是 MySQL 5.7,driver-class-name 可以用 com.mysql.jdbc.Driver,但 MySQL 8.0 必须用 com.mysql.cj.jdbc.Driver。项目里 pom.xml 引入的 mysql-connector-java 版本如果是 8.x,那驱动类名就必须写带 cj 的版本,否则启动会直接报找不到驱动类。
5.3 前端请求报跨域错误,但后端明明配置了 CORS
现象:浏览器 Console 报Access-Control-Allow-Origin is missing,接口请求显示 fail。原因:后端的 CORS 配置用的是@CrossOrigin注解,但你可能只加在了某一个 Controller 上,而其他 Controller 没有加。或者前端请求的 URL 根本就不是后端实际地址,被代理挡掉了。解决:我建议统一在 WebMvcConfigurer 里配全局 CORS,不要每个 Controller 单独加注解。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }但注意,一旦前端配置了代理,跨域问题实际上被 devServer 解决了,根本不需要后端开 CORS。如果你同时配置了代理和后端 CORS,容易出现预检请求 OPTIONS 被拦截的情况。所以原则是:开发环境走代理,生产环境走 Nginx,后端的 CORS 只作为兜底方案。
5.4 Vue 打包后部署,页面白屏或刷新 404
现象:本地 npm run dev 一切正常,npm run build 后用 Nginx 部署,打开首页是空白页,或者刷新子路由时 Nginx 返回 404。原因:Vue 路由默认是 history 模式,刷新一个非根路径时,Nginx 找不到对应的物理文件。解决:在 vue.config.js 里把 publicPath 改成相对路径,并配置 Nginx 的 try_files 指令。
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }publicPath 我记得改成 './' 后静态资源引用会相对当前路径,能解决一部分白屏,但 history 模式的刷新问题必须靠 Nginx 的 try_files 解决。如果你不想折腾 Nginx,最简单的方案是把路由模式改成 hash,缺点就是 URL 里会多一个 # 号。
6. 从一个真实需求出发:把“活动不限制人数”改成“满员自动关闭”,我改了哪些文件
我实际遇到过一个改动需求:客户的社团活动原本不做人数限制,后来发现报名人数太多现场坐不下,要求改成设置人数上限、满员后就提示“活动已满”。这个改动思路很典型,我拆开讲一下。
第一步是改数据库,activity 表加一个 max_people 字段,类型 int,默认 0 表示不限制。这个操作不要手动在 Navicat 里点,直接把 SQL 写进初始化脚本里,保证换一台电脑导入脚本后表结构一致。
ALTER TABLE activity ADD COLUMN max_people INT DEFAULT 0 COMMENT '活动人数上限,0不限制';第二步是改报名接口,报名前查当前有效报名人数,和 max_people 比较,超过就返回错误。同时要注意乐观锁,防止并发超卖。我在这个项目里用了最简单的方式,查询当前人数时直接聚合统计有效报名记录,不把人数冗余到 activity 表里,避免维护两处数据的一致性。
第三步是改前端,活动详情页在人数已满的状态下禁用报名按钮,显示“活动已满员”。这里有个细节,前端按钮禁用只是体验优化,后端校验才是真正的防线。我见过太多前端同学只改了按钮不可点,接口照样被 Postman 模拟请求打穿。
这类改动看起来简单,但涉及数据库迁移、后端校验、前端状态提示三层。只要有一层没同步,用户就会看到“还能报名”,提交后却得到“活动已满”的提示,体验很差。从那以后我每次接手类似系统,都会先梳理数据表的状态字段和接口的状态判断,确认没有一层是黑匣子,再动手改代码。宁可多花半小时把流程画清楚,也不愿意上线后被人说“这系统怎么这么玄学”。希望帮到你。
本文还有配套的精品资源,点击获取