SpringBoot+Vue社团管理系统:从数据库设计到前后端联调全攻略
2026/9/18 3:13:02 网站建设 项目流程

每年到做课程设计或者毕设选题的时候,“社团管理系统”一定是出现频率最高的那一批。题目虽然老,但说实话,大多数同学写出来的版本都卡在同一个地方:后端增删改查写完了,前端一联调就各种报错,跨域、路由模式、版本不兼容,折腾一个星期连登录页都跳不过去。我最近把一套基于 SpringBoot + Vue + MyBatis + MySQL 的校园社团信息管理系统重新完整过了一遍,从数据库设计到前后端联调再到部署启动,中间踩了不少坑,也总结了一些可以让后来者少走弯路的经验。这篇文章就把这套系统的设计思路、表结构、核心代码和排错过程完整梳理出来,给正在做社团管理系统或者想从传统 JavaWeb 过渡到前后端分离开发的同学一份可以直接参考的样本。

这个系统并不是什么特别高深的大项目,但它非常典型:涉及用户角色、社团信息、入社申请、活动报名、公告管理,业务关系丰富,正好能覆盖全栈开发的大部分基础知识。后端我采用 SpringBoot + MyBatis + MySQL,前端用 Vue3 + Element Plus,整体是前后端分离的开发模式。功能上包含学生注册登录、社团列表浏览、提交入社申请、管理员审批、活动发布与报名、公告通知等闭环流程。如果你需要一个既能讲清楚技术、又能顺利跑起来的课设或毕设项目,这套技术选型和代码结构可以直接套用。

1. 为什么选这个系统练手:需求拆解与技术选型逻辑

1.1 校园社团业务的真实痛点

在没有管理系统之前,校园社团的日常运营基本都是靠人肉维护的:新生加入社团要填纸质报名表,社长收集完再手动录入 Excel;活动通知发在群里,靠接龙统计人数;社团成员名单几个学期不更新,到底哪些人还在社团都说不清楚。这些场景总结起来就是三个核心问题:信息分散、流程不透明、数据难统计。

所以社团信息管理系统要解决的,不是单点的增删改查,而是把“学生—社团—活动”这条线理清楚。学生要能浏览社团、提交申请、查看自己的申请进度;管理员要能维护社团信息、审核入社申请、发布活动、管理成员;系统本身还要能记录活动报名情况,让每次统计都有数据可查。

1.2 功能模块怎么拆

围绕上面的痛点,我把系统拆成了下面几个模块:

模块核心功能主要角色
登录注册账号密码登录、新用户注册、角色区分学生、管理员
社团管理社团列表、社团信息维护、负责人信息管理员
入社申请学生提交申请、管理员审核、状态跟踪学生、管理员
成员管理社团成员列表、移除成员、成员统计管理员
活动管理活动发布、活动列表、活动报名学生、管理员
公告管理公告发布、公告展示管理员、学生

实际开发时,模块与模块之间有大量关联操作。比如“入社申请”前端只是提交一条记录,后端审核通过时却要同步做两件事:更新申请状态、插入成员记录。这种关联业务正是后端设计能力的最好练习。

1.3 技术选型的判断

选 SpringBoot + Vue + MyBatis + MySQL,并不是因为它有多新,而是这套组合最适合这个场景。

SpringBoot 的核心价值是简化配置。传统 SSM 要写一堆 XML 配置,SpringBoot 通过自动配置把大部分工作省掉了,内置 Tomcat 也让部署变得更简单。对于课程设计和毕设来说,SpringBoot 几乎成了事实标准,面试问 SpringBoot 的频率也比 SSM 高得多。

MyBatis 的优势在于 SQL 可控。社团列表按分类筛选、活动按关键字搜索、报名人数统计,这类带动态条件的查询用 MyBatis 的 XML 写非常顺手,一眼就能看到最终执行的 SQL。相比 JPA 那种全自动映射,MyBatis 对初学者更友好,排查问题时也更直观。

Vue 生态成熟,Element Plus 组件库开箱即用,表格、表单、弹窗、分页这些后台管理系统最常用的界面组件都能直接拿来组合,节省大量前端样式时间。MySQL 则不用多说,主流、免费、资料多,学生本机装一个 8.0 版本完全够用。

2. 数据库先行:社团核心业务的数据模型设计

2.1 表结构总览

很多同学做管理系统上来就写接口,写到一半才发现表之间关系没理清,又回头改表,非常痛苦。数据库设计应该是整个项目的第一步,至少要把下面这些表设计清楚:

  • user:用户表,存学生和管理员的账号信息、角色标识
  • club:社团表,存社团名称、分类、简介、负责人
  • club_member:社团成员表,记录用户和社团的从属关系
  • join_apply:入社申请表,记录学生提交的申请和审批状态
  • activity:活动表,存活动标题、时间、地点、内容
  • activity_join:活动报名表,记录谁报名了哪个活动
  • notice:公告表,存公告标题、内容和发布时间

这七张表是核心,其它像社团分类如果想做规范化,也可以单独拆一张表,但课设阶段用字段存储字符串分类就够了,没必要过度设计。

2.2 核心建表 SQL

拿最重要的三张表举例。用户表要注意username加唯一索引,防止重复注册;roletinyint区分学生和管理员,1 表示学生,2 表示管理员。

CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `role` tinyint DEFAULT 1 COMMENT '1学生 2管理员', `real_name` varchar(50) DEFAULT NULL, `student_no` varchar(20) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

社团表club里我用president_id关联创建者,这个设计比在社团表里直接存一个“负责人姓名”字符串更合理。因为姓名会变,但如果只存 ID,展示时就要多一次关联查询。做课设时展示负责人姓名可以接受冗余,但设计时还是建议用 ID 去关联。

CREATE TABLE `club` ( `id` int NOT NULL AUTO_INCREMENT, `club_name` varchar(100) NOT NULL, `category` varchar(50) DEFAULT NULL, `intro` text, `president_id` int DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

入社申请表是业务状态最复杂的一张表,它需要记录申请的学生、申请的社团、申请理由、当前状态和审核时间。这里的状态我用0/1/2表示待审核、已通过、已拒绝,代码里用 Integer 字段接收就行,不要用字符串去存'PENDING'这类单词,既费空间又容易写错。

CREATE TABLE `join_apply` ( `id` int NOT NULL AUTO_INCREMENT, `club_id` int NOT NULL, `user_id` int NOT NULL, `reason` varchar(255) DEFAULT NULL, `status` tinyint DEFAULT 0 COMMENT '0待审核 1通过 2拒绝', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP, `audit_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_club_status` (`club_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

club_memberactivity_join都是典型的多对多关联表,结构类似。成员表关联社团和用户,活动报名表关联活动和用户,两张表都只需要保留 ID 和加入时间即可。

2.3 关系梳理与索引建议

这个系统的数据关系并不复杂:用户和社团是多对多,一个学生可以加入多个社团,一个社团有多个成员,中间表就是club_member;用户和活动也是多对多,通过activity_join关联;社团和活动是一对多,一个社团可以发布多个活动。

关于外键,我的建议是:不建物理外键,而是在代码层面保证逻辑完整。原因有两个:一是物理外键在删除数据时非常容易报错,课设阶段经常需要手工清理测试数据,外键会让人抓狂;二是实际企业项目里,很多团队也默认不用物理外键,靠事务和业务逻辑来保证一致性。

索引方面,除了主键之外,我会在查询频率高的字段上加索引。比如join_apply表上(club_id, status)联合索引,因为管理员最常查的就是“某个社团下待审核的申请列表”;activity表上club_id加普通索引,因为活动列表经常按社团过滤。

3. 后端落地:SpringBoot与MyBatis的整合细节和业务实现

3.1 版本选择是第一关

后端踩坑率最高的地方,反而不是业务代码,而是 SpringBoot 版本选择。现在网上新教程动不动就上 SpringBoot 3.x,但如果你的 JDK 还是 1.8,直接复制依赖下来,项目根本启动不了,因为 SpringBoot 3.x 强制要求 JDK 17 及以上。

我的建议是,如果机器上装的是 JDK 8,稳妥选择 SpringBoot 2.7.18,搭配mybatis-spring-boot-starter2.3.2,这是经过大量项目验证的稳定组合。如果已经安装了 JDK 17,也可以用 3.x,但要注意 SpringBoot 3 里javax包名改成了jakarta,很多老代码里的import javax.servlet全部要换,网上的教程如果没提这一点,你抄下来大概率编译不过。

创建项目可以用 IDEA 自带的 Spring Initializr,也可以用官网的初始化页面,把项目名和依赖选好,下载下来解压导入 IDEA。下面是我在pom.xml里放的核心依赖:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

3.2 基础配置与 SQL 日志打印

application.yml是整个后端配置的中心。数据源配置、MyBatis 映射文件路径、驼峰映射、SQL 日志打印全都写在这里。我通常还会把allowPublicKeyRetrieval设为true,因为 MySQL 8.0 在某些连接方式下不开启这个参数会直接报错。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/club_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.club.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

重点说两个配置:map-underscore-to-camel-case一定要开启,这样数据库里的club_name字段能自动映射到 Java 实体类的clubName属性,少写很多resultMaplog-impl设置为StdOutImpl后,IDEA 控制台会直接打印每条 SQL 和参数,联调时定位问题事半功倍。

3.3 Mapper 接口、XML 与动态 SQL

MyBatis 的使用方式一般是接口加 XML。例如活动列表查询,前端会传社团 ID、关键字等多个可选参数,用动态 SQL 的<where><if>标签可以优雅地组合条件:

<select id="selectActivityList" resultType="com.example.club.entity.Activity"> SELECT * FROM activity <where> <if test="clubId != null"> AND club_id = #{clubId} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR content LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY start_time DESC </select>

注意这里模糊查询没有用LIKE '%${keyword}%',而是用CONCAT拼参数,#{}会预编译成占位符,能有效防止 SQL 注入。新手最容易犯的错误就是把用户输入直接拼进 SQL 字符串,这在管理系统里是大忌。

Mapper 接口只需要声明方法和@Param参数即可:

@Mapper public interface ActivityMapper { List<Activity> selectActivityList(@Param("clubId") Integer clubId, @Param("keyword") String keyword); }

3.4 Service 事务与 MyBatis 缓存陷阱

后端的核心业务基本都放在 Service 层。以“管理员审批入社申请”为例,这个操作要同时更新申请状态并插入成员记录,两步必须要么都成功、要么都失败,所以一定要加@Transactional

@Service public class JoinApplyServiceImpl implements JoinApplyService { @Autowired private JoinApplyMapper joinApplyMapper; @Autowired private ClubMemberMapper clubMemberMapper; @Override @Transactional(rollbackFor = Exception.class) public void audit(Integer applyId, Integer status) { JoinApply apply = joinApplyMapper.selectById(applyId); if (apply == null) { throw new BusinessException("申请记录不存在"); } joinApplyMapper.updateStatus(applyId, status); if (status == 1) { ClubMember member = new ClubMember(); member.setClubId(apply.getClubId()); member.setUserId(apply.getUserId()); clubMemberMapper.insert(member); } } }

关于@Transactional有一个特别经典的坑:如果你在方法里用 try-catch 把异常吞掉了,Spring 感知不到异常,事务就不会回滚。所以rollbackFor = Exception.class虽然写了,但前提是你的异常要抛出到事务代理层。另外,同一个类内部方法调用this.audit()是不经过代理的,事务可能不生效,需要注意。

还有一个和缓存相关的坑。MyBatis 默认开启一级缓存,缓存级别是 SqlSession。在 Spring + 事务的组合下,同一个事务里的多次查询会共用一个 SqlSession,如果你在事务里先查了某条数据,修改后再查同一条数据,拿到的可能是旧值。这种情况最容易发生在“先判断再更新”的组合操作里。常规做法是保持事务边界足够短,不需要事务的查询就不要放到同一个事务方法里。二级缓存默认关闭,课设项目也不建议开,因为一旦数据被多表更新,缓存刷新时机控制不好,会出现脏读,排查起来非常痛苦。

4. 前端构建:Vue3 + Element Plus从安装到页面渲染

4.1 环境准备

前端部分我用的 Vue3 加 Vite 构建工具。环境要求 Node.js 18 或更高版本,装完 Node 后自带 npm。很多同学卡在npm install这步,下载慢、报各种错,我习惯先把 npm 镜像源切到国内镜像,然后再安装依赖,会快很多。

创建项目推荐用 Vite 官方脚手架:

npm create vite@latest club-system-frontend -- --template vue cd club-system-frontend npm install npm install element-plus axios vue-router@4 pinia

Element Plus 是一个 Vue3 组件库,表格、表单、按钮、对话框这些后台常用组件都有,而且风格统一,不需要自己调试繁琐的 CSS。Axios 用于发起 HTTP 请求,Vue Router 管理路由,Pinia 管理全局状态。对于课设项目,Pinia 主要用来存用户信息,不装也行,但装了会显得结构更规范。

4.2 页面布局与路由

管理后台的经典布局是左侧菜单、右侧内容区。我用一个DefaultLayout.vue做整体框架,里面放左侧侧边栏和顶部导航,中间用<router-view />渲染子页面。路由配置要支持懒加载,这样首屏加载会快一些:

import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/login' }, { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/layout', component: () => import('@/layouts/DefaultLayout.vue'), children: [ { path: '/club', component: () => import('@/views/club/ClubList.vue') }, { path: '/activity', component: () => import('@/views/activity/ActivityList.vue') }, { path: '/join', component: () => import('@/views/join/JoinList.vue') } ] } ] export default createRouter({ history: createWebHashHistory(), routes })

这里我特意用了createWebHashHistory也就是 hash 模式,而不是 history 模式。原因是 history 模式部署到服务器后,直接刷新子路由页面会出现 404,需要额外配置服务端重写规则。课设项目大多没有复杂的服务端环境,hash 模式最省心。

4.3 Axios 封装与代理

前端调接口不能直接用http://localhost:8080去请求,会遇到跨域问题。开发环境最优雅的解决方案是配置 Vite 代理,让前端把请求发给同源地址,由 Vite 开发服务器转发到后端。

vite.config.js中这样配置:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ base: './', plugins: [vue()], server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/club/list,Vite 会自动转发到http://localhost:8080/api/club/list。后端接口统一以/api开头,就避免了联调时改来改去。

同时封装一个 Axios 实例,把 token 注入到请求头,统一处理后端返回的错误码:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { router.push('/login') } ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default request

4.4 一个列表页和申请表单的写法

社团列表页是最典型的 Element Plus 组合。顶部放查询表单,下面放表格,操作列里放按钮。筛选条件通过query对象传给后端,拿到数据后用el-table渲染。

<template> <el-card> <el-form :inline="true"> <el-form-item label="社团名称"> <el-input v-model="query.keyword" placeholder="请输入关键词" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadClubs">查询</el-button> </el-form-item> </el-form> <el-table :data="clubList" v-loading="loading" border stripe> <el-table-column prop="clubName" label="社团名称" min-width="140" /> <el-table-column prop="category" label="分类" width="100" /> <el-table-column prop="presidentName" label="负责人" width="100" /> <el-table-column prop="intro" label="简介" min-width="220" show-overflow-tooltip /> <el-table-column label="操作" width="120"> <template #default="{ row }"> <el-button type="primary" link @click="openApply(row)">申请入社</el-button> </template> </el-table-column> </el-table> </el-card> </template>

申请入社的弹窗里有一个需要特别注意的点:如果用了日期选择器el-date-picker,一定要设置value-format="YYYY-MM-DD",否则你绑定到表单上的会是一个 Date 对象,提交给后端时序列化出来的格式可能不是你想要的,后端解析 LocalDate 时容易报错。

5. 完整业务链路:申请入社、审批、活动发布的前后端串联

5.1 登录、角色和接口统一约定

前后端交互要有一个统一约定,不然很容易乱。我这里用的是简单的 Token 方案:用户登录成功后,后端生成一个 Token 字符串返回给前端,前端存到localStorage里,之后每次请求都放在Authorization请求头中。后端用一个拦截器校验Authorization,没有或无效就直接返回 401。

接口统一以/api开头,返回格式做成固定的Result对象:code表示状态,message表示提示信息,data存放业务数据。前端 Axios 封装里直接取response.data,再根据code判断业务是否成功。

角色权限上,学生和管理员调用的接口要区分开。管理员的接口,比如审批入社、发布活动、维护社团信息,后端接口上要做角色校验,不能只靠前端把按钮藏起来。安全校验要放在后端,这是一个非常基本的红线。

5.2 学生申请入社:接口链路和状态流转

学生点击“申请入社”后,前端调用POST /api/join/apply,参数是clubIdreason。后端 Service 要做两个检查:一是当前用户是否存在未处理的申请记录,防止重复申请;二是该学生是否已经是社团成员,防止重复加入。

一条申请的生命周期是这样的:

场景前端动作后端接口数据库状态变化
学生提交申请点击申请入社,填写理由POST /api/join/applyjoin_apply.status = 0
管理员查看待审批列表进入审核页面GET /api/join/list?status=0无变化
管理员点击通过确认通过POST /api/join/auditjoin_apply.status = 1,插入 club_member
管理员点击拒绝确认拒绝POST /api/join/auditjoin_apply.status = 2

前端页面需要根据status显示不同标签:0 显示“待审核”并高亮,1 显示“已通过”用绿色,2 显示“已拒绝”用灰色。这个标签切换在 Element Plus 里用el-tag:type绑定即可。

5.3 管理员审批:一改两插的事务操作

审批接口是后端最体现事务价值的地方。前端每次只传applyId和审批结果,后端拿到申请记录后,判断如果通过,就同时插入club_member记录。这个操作我在前面已经贴过代码,这里补充一个很容易被忽略的问题:审批通过前还要再查一次申请状态,防止管理员在页面停留太久,两个人同时操作同一条申请,导致重复插入成员。

解决方式是在更新申请状态时加上条件,例如UPDATE join_apply SET status = 1 WHERE id = #{applyId} AND status = 0,如果影响行数为 0,说明已经被处理过,直接抛异常提示“该申请已处理”。这种“乐观锁”思路在林管理系统里很实用。

5.4 活动发布与报名防重校验

活动发布相对简单,管理员填活动标题、时间、地点、内容,提交后插入activity表。学生端查看活动列表,点击“报名”时调用POST /api/activity/join

这里的高频坑是重复报名。学生手快点了两次按钮,前端可能因为网络问题没有立刻禁用按钮,后端就会收到两条报名请求。所以后端必须做防重校验:在插入activity_join之前,先按activityIduserId查一次,如果已存在就直接返回“您已报名该活动”。更稳妥的做法是在activity_join表上建(activity_id, user_id)唯一索引,数据库层面兜底,双保险。

6. 实测踩坑:版本冲突、缓存不生效、打包布局异常怎么查

6.1 SpringBoot 版本太高导致项目启动失败

我第一次跑了一个学弟给的源码,他用的 SpringBoot 3.2,我的本机 JDK 是 1.8,结果 IDEA 启动时直接报java.lang.UnsupportedClassVersionError,后续还有一堆关于javax.servlet找不到的错误。这个问题的根源就是版本匹配不上。

如果你也是 JDK 1.8,建议把 SpringBoot 降级到 2.7.18。操作方法很简单,修改父工程的版本号,然后刷新 Maven 依赖,顺利的话项目就能直接启动。如果代码里用了jakarta.*的包,还要把相关 import 改回javax.*。反过来,如果你已经在用 JDK 17,就别再复制 SpringBoot 2.7 的老教程了,直接用 3.x 即可。做项目前先确认版本组合,能省掉一整天的排查时间。

6.2 Vue 打包后静态资源 404 和布局异常

开发环境一切正常,npm run build打包后部署到服务器,打开页面发现空白,控制台全是 JS、CSS 加载 404。这个坑几乎每个用 Vite 打包的同学都遇到过,原因是 Vite 默认base/,打包后的资源路径是绝对路径,部署到服务器非根目录时全部失效。

解决办法很简单,在vite.config.js中把base设为'./',让资源路径变成相对路径:

export default defineConfig({ base: './', // ... })

还有一个和布局相关的坑:打包部署后页面能打开,但表格错位、样式挤在一起。这通常是路由模式导致的,history 模式刷新子路由时,服务器返回 404 页面,界面看起来就像“布局异常”。改用 hash 模式后基本能解决。另外 Element Plus 的el-table在某些情况下出现列错位,给表格加一个响应式key,或者动态调用doLayout()方法可以强制刷新表格布局。

6.3 MyBatis XML 中的特殊符号与更新子查询

MyBatis 的 XML 文件里,<>这些符号会被 XML 解析器当成标签开始,直接写会报错。最简单的处理是用转义符&lt;&gt;,或者用 CDATA 包裹:

<select id="selectExpiredActivities"> SELECT id FROM activity WHERE start_time &lt; NOW() </select>

另一个非常容易踩的 MySQL 坑是“更新子查询”。如果你想在一张表里根据子查询更新同一张表,比如把某个社团所有待审核申请批量改成拒绝,直接写会报错:You can't specify target table 'join_apply' for update in FROM clause。因为 MySQL 不允许直接对UPDATE的目标表再执行子查询。

解决办法是套一层临时表派生表查询:

UPDATE join_apply SET status = 2, audit_time = NOW() WHERE id IN ( SELECT tmp.id FROM ( SELECT id FROM join_apply WHERE status = 0 AND club_id = #{clubId} ) tmp );

这种写法之所以能绕过限制,是因为外层查询的FROM已经变成了一张临时表tmp,不再直接引用更新目标表。这个小技巧在批量处理业务数据时非常常用。

6.4 前后端联调中的跨域

虽然开发环境配了 Vite 代理,基本不会触发跨域,但如果你的前端没有走代理而是直连后端地址,就会遇到浏览器跨域报错。这里给后端加一个全局的CorsConfig,允许前端地址跨域访问。注意,开发环境用代理就够了,生产环境建议由网关或 Nginx 统一处理跨域,不要依赖后端的 CORS 配置做唯一手段,因为它相当于允许了跨源穿行,安全上需要谨慎。

7. 本地启动项目:从空环境到跑起来的最小操作清单

7.1 环境版本清单

如果你打算在自己的电脑上把项目完整跑起来,先对照一下环境版本:

软件版本建议说明
JDK1.8 或 171.8 配 SpringBoot 2.7,17 配 3.x
Maven3.6.3 及以上IDEA 自带也可以
Node.js18 LTS 及以上Vite 构建必需
MySQL8.05.7 也可以,注意驱动版本
IDEIDEA + VS Code不是必须,但推荐

7.2 初始化数据库

在 MySQL 中新建数据库club_system,字符集选择utf8mb4,然后把建表 SQL 脚本执行一遍。如果数据还需要初始演示数据,就一并导入部分测试社团和管理员账号。用命令行执行:

mysql -uroot -p source D:/club_system.sql;

注意脚本里如果有中文注释,数据库连接要确认使用 UTF-8,否则容易出现乱码。

7.3 启动后端

用 IDEA 导入后端项目后,等待 Maven 下载依赖。然后检查application.yml里的数据库账号密码是否和你本机一致,最后运行启动类的main方法。看到控制台打印“Tomcat started on port(s): 8080”就代表启动成功了。

建议启动时先看一眼控制台里的 SQL 日志是否正常打印。如果什么 SQL 都没有,可能是 Mapper XML 没有被扫描到,检查一下mapper-locations配置和resources/mapper目录是否匹配。

7.4 启动前端

前端项目导入 VS Code 后,在终端执行:

npm install npm run dev

看到 Vite 打印出本地访问地址http://localhost:5173,说明前端启动成功。用初始化的管理员账号登录后台,测试“社团列表—申请入社—管理员审批—成员列表”这条主流程,如果都能走通,整套系统就正式跑起来了。

我在实际测试时更习惯把后端、前端、数据库三个窗口同时打开,原因很朴素:任何一个环节报错,控制台信息都能第一时间看到,不用来回猜。尤其是判断前后端接口是否调通,直接在浏览器开发者工具的 Network 面板里看请求状态码和响应体,比不停去翻代码效率高得多。这套系统做完之后,我的个人感受是:真正的收获不在于“会用某个框架”,而在于搞清楚了数据在数据库、后端接口、前端页面之间是怎么流动的。当你把一条申请记录从学生点击按钮一直追踪到数据库表的 status 字段变化,前后端分离开发这个概念才算真正落地了。

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

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

立即咨询