SpringBoot大学生社团管理系统开发全流程实战:从搭建到避坑部署

最近又有人拿着“springboot大学生社团管理系统”这个题目来找我看代码,这几乎是计算机毕业设计里每年必出现的方向,但每年都有同学在同样几个地方栽跟头。有的卡在项目创建时版本选错,JDK和SpringBoot对不上,一启动就报错;有的卡在Mapper扫描不到、页面白屏;还有的做完基础增删改查后发现流程漏洞太多,答辩被老师一问就露馅。这篇文章我就把这个项目从需求拆解到落地部署完完整整讲一遍,覆盖技术选型、数据库设计、登录认证、拦截器校验、分页查询、审核流程还有部署上线,过程中会把我在实际调试里踩过的坑一并列出来。准备拿这个题目做毕设、或者想借一个真实项目吃透SpringBoot开发套路的朋友,这篇文章可以直接当操作手册来用。

1. 先看清这个毕设的本质

1.1 一个标题三副面孔:系统到底要做什么

“计算机毕业设计springboot大学生社团管理系统”“基于SpringBoot的高校学生社团信息化管理平台”“基于Java Web的校园社团活动与服务系统”,这三个标题听起来不一样,本质上说的是同一个东西:一个面向高校学生、学生社团和系统管理员角色的校园社团信息管理平台。

核心业务可以归纳成几条线。学生端要做的事情是注册账号、浏览社团列表、查看社团详情、申请加入社团、报名活动、查看自己的活动记录。社团管理员端要做的事情是创建和管理本社团的基本信息、发布活动、审核成员加入申请、审核活动报名、发布社团公告。系统管理员端要做的事情是审核社团创建申请、管理全部用户、统计社团和活动数据、处理违规内容。

很多同学一拿到题目就急着写代码,这是最大的误区。毕设项目首先是个“管理系统”,管理两个字意味着数据从录入、流转、审核到归档,每一步都要有逻辑。比如学生申请加入社团,这个申请从提交到通过需要走一个状态变化;活动发布后有人报名,报名人数不能超过活动名额。这些流程想清楚了,数据库字段表结构自然就出来了,代码反而不是最难的部分。

1.2 为什么这个题目年年有人选

这个题目的存活率高是有道理的,它不是特别难,但也不是无脑的纯增删改查,难度刚好卡在能锻炼人又不至于做不出来的区间。

第一,业务场景清晰。高校社团管理是所有人都有生活经验的事,需求不用凭空编。学生、社团、活动、报名、公告、审核,这些概念在现实里都能对上号,做设计的时候不容易跑偏。第二,技术覆盖得全面。一个完整的社团管理系统会用到用户认证、权限拦截、CRUD、分页、一对多多对多关联查询、异常处理、数据校验,这些都是Java Web开发的核心知识点,面试和答辩都能聊。第三,扩展空间大。基础版做完之后,随便加一个功能都能成为亮点,比如活动签到、数据可视化大屏、Excel导出、消息通知,这些都是同一个题目下的加分项。

所以如果你还没定题,这个方向是可以选的;如果已经选定了,就踏踏实实把流程做完整,别只顾着堆页面。

1.3 为什么大家最后都选了SpringBoot

很多教材还在讲SSH(Struts2+Spring+Hibernate)或者SSM(Spring+SpringMVC+MyBatis),但到了真正动手做项目,几乎没人再手动搭SSM框架了,原因很现实:配置太多,效率太低。

SpringBoot最核心的价值是“自动配置”和“起步依赖”。你引入一个spring-boot-starter-web,它就帮你把SpringMVC、内嵌Tomcat、JSON转换这些基础配置全部准备好;引一个mybatis-plus-boot-starter,数据访问那一层的配置也就齐了大半。你不需要再写一堆XML配置文件去定义Bean,只需要在application.yml里写上必要的信息,比如端口、数据库连接,项目就能跑起来。

很多人担心用SpringBoot会不会丢了底层知识,其实不会。SpringBoot底层还是Spring那一套,依赖注入、AOP、事务管理全都在,只是把重复劳动简化了。答辩的时候老师问“SpringBoot和SpringMVC什么关系”“自动配置原理是什么”,你能答清楚,说明你不仅会用,还理解原理。这也是我在后面几节反复强调版本、目录、注解这些细节的原因,因为细节才是拉开分差的地方。

需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。

2. 系统整体设计:先画清楚再动手

2.1 三种角色和权限边界

社团管理系统至少要有三种角色:学生、社团管理员、超级管理员(系统管理员)。

这里要先明确权限边界,否则后面写拦截器的时候会乱。我见过很多代码,登录是做了,但进入后台管理页面的判断只有“是否登录”,没有“角色是否匹配”,结果普通学生也能进管理页,这就是明显的设计缺陷。

我的建议是这样划分。

学生能做的事情:修改自己的个人资料、浏览所有已审核通过的社团、查看社团详情、申请加入社团、退出社团、浏览活动列表、报名活动、查看自己的报名状态。

社团管理员能做的事情:管理隶属自己的一个或多个社团的基本信息、发布和编辑社团活动、审核加入本社团的申请、审核报名本社团活动的申请、发布社团公告。需要强调“隶属自己”,也就是说一个社团管理员不能去删别人的社团,这是很常见的数据越权问题。

超级管理员能做的事情:审核社团创建申请、禁用或删除违规用户、冻结违规社团、给所有社团发系统公告、查看全站统计报表。

这道权限边界要在数据库设计阶段就体现出来,用户表里要有role字段,社团表和用户表之间要有创建者关联,成员关系表里要有状态字段。权限控制要在后端拦截器和服务层做双重校验,前端隐藏按钮只是体验优化,不能作为安全手段。

2.2 功能模块怎么拆

我通常把系统拆成六个模块:用户模块、社团模块、活动模块、报名审核模块、公告模块、统计模块。

用户模块负责注册、登录、个人信息修改和密码重置。注册环节要注意密码加密,一定不要明文存数据库,Spring Security的BCryptPasswordEncoder或者Hutool里的BCrypt都可以,哪怕不做那么复杂,至少加个盐做SHA-256,这是答辩老师特别喜欢问的点。

社团模块包含社团分类、社团创建申请、社团信息编辑、社团成员列表、成员退出和移除。社团创建不能直接生效,要有一个待审核的状态,由超级管理员审核通过之后,这个社团才会出现在前台列表里。这个流程符合现实,也能体现你的逻辑设计能力。

活动模块包含活动发布、活动编辑、活动列表、活动详情、报名截止时间、活动名额和已报名人数。发布活动之后要能防止重复提交,也要判断活动开始时间和当前时间的关系。

报名审核模块是系统的关键,学生报名活动后,状态应该是“待审核”或者“报名成功”,如果社团管理员设置了需要审核,那么状态流转就是待审核到通过/驳回,如果不需要审核,则直接占用名额。

公告模块就是简单的发布和展示,可以按角色区分:系统公告所有人可见,社团公告只有加入该社团的人可见。

统计模块可以作为加分项,展示社团数量、成员数量、活动数量、活动参与人次,按月份统计趋势,这里用ECharts画几个图表,效果非常好。

2.3 数据库设计:核心表和字段关系

数据库设计决定整个项目的上限,表字段建不好,后面写关联查询的时候想哭都哭不出来。

一个常见的核心表设计如下。用户表叫sys_user,字段包括id、username、password、real_name、student_no、phone、email、role、avatar、create_time、update_time、deleted。这里deleted是逻辑删除位,0表示正常,1表示删除,逻辑删除在毕设项目里比物理删除更好,数据可回溯,老师也认可这种设计。

社团表叫club,字段包括id、name、introduction、category_id、logo、leader_id、status、create_time、update_time、deleted。leader_id外键关联社团管理员的用户ID。status可以定义成0待审核、1已通过、2已禁用、3已驳回。

社团成员关系表叫club_member,字段包括id、club_id、user_id、role_in_club、status、join_time、create_time。这里status表示申请状态,0待审核、1已加入、2已拒绝、3已退出或者被移出。

活动表叫activity,字段包括id、club_id、title、content、location、start_time、end_time、max_participants、current_participants、status、create_time、update_time。活动状态可以设计成0未开始、1进行中、2已结束、3已取消,但这个状态最好通过时间计算,保证业务逻辑一致性。

活动报名表叫activity_register,字段包括id、activity_id、user_id、status、apply_time、audit_time、audit_remark。status是0待审核、1报名成功、2已拒绝、3已取消。报名表要加联合唯一约束,保证同一个用户对同一个活动不能重复报名。

公告表叫notice,字段包括id、title、content、type、target_club_id、create_user_id、create_time。type可以区分系统公告和社团公告。

核心的关系是:用户和社团是多对多,通过club_member表关联;一个社团有一个负责人,通过club.leader_id关联;用户和活动是多对多,通过activity_register表关联。建议在表设计文档里把字段注释都写清楚,答辩的时候直接展示ER图,很加分。

2.4 技术栈清单与版本选择

技术栈选型直接决定了你后面要踩多少坑,这里给出一套我实测下来比较稳的组合。

后端用SpringBoot 2.7.18,JDK用1.8。很多人问为什么不用最新的SpringBoot 3.x,原因很简单:3.x基于Jakarta EE,很多老版本的依赖不兼容,而且JDK要求17起步,很多学校机房和老师的电脑未必装了JDK17。毕设求稳,2.7.x是SpringBoot 2.x最后一个维护版本,功能完善,资料最多,遇到问题搜一下就有一堆答案。

数据访问层用MyBatis-Plus,版本用3.5.x。MyBatis-Plus在MyBatis之上封装了通用Mapper和通用Service,单表CRUD不需要写SQL,分页插件也好用,能省下大量时间。要注意mybatis-plus和SpringBoot的版本兼容,后面我会专门讲。

数据库用MySQL 8.x,驱动在SpringBoot 2.7.x里会自动匹配mysql-connector-java 8.0.x,连接字符串要注意使用com.mysql.cj.jdbc.Driver。

前端如果做简单模式,用Thymeleaf服务端渲染加Bootstrap就行,一个项目里不用再开Node环境;如果做前后端分离,前端用Vue3加Element Plus加Vite,后端预留统一返回结果和跨域配置。我这里两种方案都涉及,但核心代码以服务端渲染为主来演示,逻辑是一样的。

认证方案用JWT,生成一个token字符串,登录成功之后返回给前端,前端存到header里,后端用拦截器校验token。这个方案在答辩里容易讲清楚,也是现在企业开发的主流做法。

其他工具类尽量用Hutool,里面封装了日期、字符串、加密等常用工具,能减少很多造轮子的时间。

3. 核心功能实操:从搭建到跑通

3.1 创建项目和标准目录结构

打开IDEA,新建项目选择Spring Initializr,注意右侧Spring Boot版本选择2.7.18,Java版本选8,Group填com.example,Artifact填club-system。

创建完项目之后,我习惯把包结构规划成以下形式,这个结构也是很多企业项目的标准做法。

text复制com.example.clubsystem
├── config          // 配置类:拦截器、跨域、MyBatis-Plus分页
├── controller      // 控制层:接收请求、返回结果
├── service         // 业务层:写核心业务逻辑
│   └── impl         // 业务实现类
├── mapper          // 数据访问层:Mapper接口
├── entity          // 数据库实体类
├── dto             // 前端入参封装对象
├── vo              // 返回给前端的视图对象
├── common          // 公共类:统一返回结果、状态枚举、异常处理
├── utils           // 工具类:JWT、加密、日期处理
└── ClubSystemApplication.java  // 启动类

controller放接口,service放业务逻辑,mapper放数据库操作,entity和数据库表一一对应,dto和vo负责前后端数据格式转换。这里有个细节:不要让entity直接暴露给前端,尤其是用户表里的password字段,一定要用vo把密码过滤掉再返回。

启动类最好放在包的根目录下,这样SpringBoot的组件扫描默认就能覆盖到所有子包。如果你把启动类放到com.example下面,然后controller放到了com.example.club.xxx里,启动类在com.example.clubsystem里,扫描包路径依然能覆盖,但为了直观,建议启动类就在根包。

3.2 配置文件怎么写才不出幺蛾子

SpringBoot的配置文件看似简单,但不少人在这里忽略几个关键项。下面是我在项目里使用的application.yml,摘取核心部分说明。

yaml复制server:
  port: 8080
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true
    context-path: /

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/club_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: yourpassword
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.example.clubsystem.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

配置里的每个关键项我都说一下用途。driver-class-name使用com.mysql.cj.jdbc.Driver,这是MySQL 8的驱动类,以前的老驱动com.mysql.jdbc.Driver在8.x里已经废弃了,如果用了会启动报错。

url里加了useUnicode和characterEncoding=utf8,防止中文乱码;serverTimezone=Asia/Shanghai,防止日期时间比实际少了8个小时,这是很多同学容易忽略的问题。

Jackson的日期格式配置成yyyy-MM-dd HH:mm:ss,这样返回JSON里的LocalDateTime和Date字段不会变成一串数字。

multipart配置是给文件上传用的,如果社团logo、活动封面需要上传图片,没有这个配置会报文件大小超限。

mybatis-plus的map-underscore-to-camel-case打开之后,数据库的create_time能自动映射到Java的createTime,不用写一大串resultMap。log-impl打印SQL日志,调试时候非常方便,正式部署可以注释掉。

logic-delete配置是MyBatis-Plus的全局逻辑删除,实体里有个deleted字段后,删除操作执行的是update语句,查询语句会自动追加deleted=0条件。这一点务必要在配置里加,不然MyBatis-Plus默认不启用逻辑删除,逻辑删除字段形同虚设。

3.3 登录认证与Token校验:拦截器到底是干什么的

登录流程是这样的:用户提交用户名和密码,后端根据用户名查出用户,用BCrypt工具比对密码。登录成功之后,生成一个JWT token,token里可以放userId和role,然后用拦截器对所有需要登录的接口做校验。

JWT工具类核心代码大致如下。

java复制public class JwtUtil {
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("your-secret-key-please-change-123456".getBytes(StandardCharsets.UTF_8));

    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() + 1000L * 60 * 60 * 24))
                .signWith(SECRET_KEY)
                .compact();
    }

    public static Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

这里有一个很关键的点:密钥不要太短,太短启动的时候会报签名错误;不要硬编码到代码里,至少放到application.yml里用@Value注入,做个样子也是好的。token过期时间设置24小时,如果要做“记住我”就设置7天。

拦截器类实现HandlerInterceptor接口,在preHandle方法里从request的header中取出token,解析成功就放行,失败就返回401。

java复制public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if (token == null || token.isEmpty()) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}");
            return false;
        }
        try {
            Claims claims = JwtUtil.parseToken(token);
            request.setAttribute("userId", Long.valueOf(claims.getSubject()));
            request.setAttribute("role", claims.get("role"));
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"msg\":\"token无效或已过期\"}");
            return false;
        }
    }
}

拦截器要注册到WebConfig里,同时指定放行路径。

java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login", "/user/register", "/club/list");
    }
}

接口设计上我习惯把需要认证的接口统一放在/api下面,比如/api/user/profile、/api/club/manage,这样拦截器的路径配置就很清晰。登录注册接口放行,其他接口都要过token校验。如果社团管理员的接口要额外校验角色,可以在拦截器里根据路径前缀判断,也可以在Controller方法上写自定义注解实现,毕设规模用拦截器判断角色就足够了。

3.4 社团与活动的CRUD:MyBatis-Plus让代码量少一半

如果用原生MyBatis,写一个社团列表要自己写XML、resultMap、动态SQL,代码量很大。MyBatis-Plus把单表CRUD都封装好了,你的Mapper接口只需要继承BaseMapper,Service实现类继承ServiceImpl,基础的增删改查就全有了。

社团管理员的添加社团和编辑社团业务,核心代码可以这样写。

java复制@Service
public class ClubServiceImpl extends ServiceImpl<ClubMapper, Club> implements ClubService {

    @Override
    public Club createClub(Long userId, ClubDTO dto) {
        Club club = new Club();
        BeanUtils.copyProperties(dto, club);
        club.setLeaderId(userId);
        club.setStatus(0);
        club.setDeleted(0);
        this.save(club);
        return club;
    }

    @Override
    public Page<Club> pageClubs(int pageNum, int pageSize, Long categoryId) {
        LambdaQueryWrapper<Club> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Club::getDeleted, 0)
               .eq(categoryId != null, Club::getCategoryId, categoryId)
               .orderByDesc(Club::getCreateTime);
        return this.page(new Page<>(pageNum, pageSize), wrapper);
    }
}

LambdaQueryWrapper的好处是写起来简单,而且属性名是编译期检查,数据库表字段改了,代码这里会直接编译报错,比字符串写法安全很多。分页这一块,要记得配置MyBatis-Plus的分页插件,否则分页查询拿到的是全表数据。

java复制@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

这个分页插件一定要在配置类里创建,并且指定数据库类型。没有它会怎样?分页方法会执行查询所有记录然后内存里手动分页,数据量一大就卡死,而且发出的SQL没有limit,属于经典坑点。

活动模块的代码结构和社团类似,但要注意几个业务校验。发布活动的时候判断clubId是否存在、当前用户是不是该社团的管理员,开始时间不能晚于结束时间,最大人数必须大于0。更新活动的时候要判断已经有报名的用户数量,如果currentParticipants大于新的maxParticipants,这个修改就要拒绝,否则会出现报名人数超过上限的数据错误。

3.5 报名审核与名额控制:状态流转要提前设计

活动报名是整个系统里最容易出问题的点,重复报名、超额报名、活动结束还在报名,这些如果没处理,答辩的时候大概率会被问到。

我的做法是先在数据库层面加唯一约束,activity_register表里activity_id和user_id建立联合唯一索引,从底层杜绝同一个人对同一个活动报名两次。这样即使代码里漏判了,数据库也会报错兜底。

然后在代码里加三个校验。第一,校验活动状态,活动必须处于报名时间内。第二,校验名额,活动表里已经报名的人数要小于maxParticipants。第三,校验审批流程,如果活动需要审核,状态设为待审核;如果不需要审核,直接报名成功并增加当前人数。

这里的名额扣减有并发问题。两个用户同时报名最后一个名额,代码里先查再用,可能两个请求都查到还有名额,然后都插入成功,让报名人数超过上限。对于毕设项目,最直接的解决办法是给活动表加一个乐观锁版本号字段version,更新当前人数时使用update语句带上version条件,更新成功后version加1。如果影响行数为0,说明名额已被占满,提示用户报名失败。这个方案能讲明白,老师会觉得你考虑到了并发场景,是加分项。

审核状态流转建议用一张表格提前定清楚,开发的时候照着写就不会乱。

场景 原状态 操作(角色) 新状态
学生提交入社申请 学生 待审核
管理员通过入社申请 待审核 社团管理员 已加入
管理员拒绝入社申请 待审核 社团管理员 已拒绝
学生报名活动 学生 待审核或报名成功
管理员通过报名 待审核 社团管理员 报名成功
管理员拒绝报名 待审核 社团管理员 已拒绝
学生取消报名 报名成功 学生 已取消
学生退出社团 已加入 学生 已退出

状态字段用int类型,在Java侧定义一个枚举类,比如ActivityRegisterStatusEnum,里面把每个状态值和描述写清楚,代码里不要到处写魔法数字。这样团队合作也好,答辩展示也好,都能看出你写代码是有工程意识的。

4. 常见问题与排查技巧实录

4.1 版本不匹配:SpringBoot不是越高越好

每年都有人因为版本踩坑,其中最典型的问题是IDEA里新建SpringBoot项目时默认选了3.x版本,结果本机是JDK1.8,启动直接报错。SpringBoot 3.x要求JDK17及以上,这是硬门槛,换不了。

我的建议很直接:毕设就老老实实用SpringBoot 2.7.18配JDK1.8,这个搭配最稳。如果要学新特性,等毕设做完再折腾。

还有一个常见问题是mybatis-plus的版本和spring-boot 2.x对应不上。mybatis-plus 3.5.9之后的一些版本已经要求Spring Boot 3环境,所以如果你用SpringBoot 2.7,就把mybatis-plus版本固定在3.5.3.x左右。以下是我常用的一组稳定依赖版本,可以直接照抄:

  • SpringBoot 2.7.18
  • mybatis-plus-boot-starter 3.5.3.2
  • mysql-connector-java 8.0.33(由SpringBoot管理)
  • hutool-all 5.8.25
  • jjwt-api / jjwt-impl / jjwt-jackson 0.11.5

依赖冲突看Maven的依赖树,IDEA里可以直接右键项目选择Maven-Show Dependencies,或者在Terminal执行mvn dependency:tree,定位到exclusions排除掉多余依赖就行。

4.2 Mapper扫描不到与XML路径问题

启动项目时报Invalid bound statement not found,或者提示找不到Mapper,绝大多数是三个原因。

第一个原因是启动类没有加@MapperScan注解,或者Mapper接口没有加@Mapper注解。解决办法是在启动类上加@MapperScan("com.example.clubsystem.mapper"),或者每个Mapper接口上标@Mapper。二选一即可,不要重复,重复也无伤大雅,但养成好习惯更重要。

第二个原因是Mapper的XML文件没被编译到target目录,或者路径配置不对。如果你把XML放到了src/main/java/com/.../mapper下面,Maven默认只编译.java文件,XML会被忽略。解决办法是把XML放到src/main/resources/mapper目录下,然后在application.yml里配置mybatis-plus.mapper-locations: classpath*:mapper/**/*.xml。如果你的XML在java目录下,也可以在pom.xml里配置resources标签把XML也打进去,但这不是常规做法,不推荐。

第三个原因是XML里的namespace和Mapper接口全路径不匹配,或者方法id和Mapper接口方法名不一致。这类问题通常看控制台报错,会直接告诉你哪个语句找不到,按照报错信息去检查即可。

4.3 端口占用、上下文路径与404

SpringBoot项目启动时报Port 8080 was already in use,说明8080被其他进程占用了。Windows下可以在命令行执行netstat -ano | findstr 8080,找到占用进程的PID,然后taskkill /PID 你的PID /F,或者直接改application.yml里的server.port为8081、8082都行。

还有一类404问题很隐蔽,就是配置了server.servlet.context-path: /api之后,Controller里的请求路径也要带上/api前缀,模板和静态资源的引用路径也会被影响。我建议初学者不要随便配置context-path,保持默认根路径,让所有路径都在Controller的RequestMapping上管理,这样排查问题简单很多。

如果访问Controller接口返回404,先看控制台有没有类似“Map 请求路径”的日志,再看请求路径和注解路径是否一致,特别是路径里的变量名和@PathVariable参数名是否对得上。有时候是跨服务调用时端口写错,这些都要逐一排查。

4.4 前后端跨域与Token失效

做前后端分离的时候,前端跑在localhost:5173,后端跑在localhost:8080,浏览器会拦截跨域请求。解决办法是后端加一个全局跨域配置。

java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

注意allowedOriginPatterns和allowCredentials不能同时使用通配符*,所以才用allowedOriginPatterns("*"),这是SpringBoot比较新版本的标准写法。跨域配置添加后,前端发起OPTIONS预检请求时,后端拦截器要放行OPTIONS请求,否则预检请求被拦截器挡住,一样会报跨域错误。

token失效这个问题常见于登录后无法获取用户信息。排查思路是:前端是否在请求头里带了token,后端拦截器里是否用request.getHeader("token")取得和前端一致的key。如果前端把token放在localStorage,请求拦截器里一定要手动设置header,很多同学忘记这一步,导致token一直为空。

4.5 中文乱码、XSS过滤与文件上传

中文乱码问题在SpringBoot里一般有三处。第一处是数据库连接的URL没有加characterEncoding=utf8,第二处是服务端响应没有设置UTF-8编码,第三处是前端页面本身的charset不是UTF-8。SpringBoot 2.x里只要配置了server.servlet.encoding.force=true,再在数据库URL里加上characterEncoding=utf8,大部分乱码都能解决。前提是数据库表本身的字符集也要是utf8mb4,建库的时候写清楚,比如CREATE DATABASE club_system DEFAULT CHARACTER SET utf8mb4。

XSS过滤是很多毕设忽略的点。热搜里有人问“上传PDF时XSS处理”,说明老师现在也会关注安全问题。我的建议是写一个全局过滤器,把所有请求参数里的尖括号等危险字符转义,或者引入一个开源的XSS过滤器工具类,对请求体里的脚本标签做清洗。至少要把