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

资讯详情

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

SpringBoot+Vue高校学科竞赛平台:从数据库设计到部署上线实战

SpringBoot+Vue高校学科竞赛平台:从数据库设计到部署上线实战

做高校竞赛管理这块系统,市面上能找到的完整源码不少,但大多是教学级Demo,要么报名流程跑不通,要么评审环节干巴巴就一个增删改查。今天分享的这套基于SpringBoot+Vue+MySQL+MyBatis的高校学科竞赛平台,是我在实际项目中从零梳理并落地过的完整实现,覆盖了竞赛发布、在线组队报名、材料审核、评委评分、成绩公示、证书管理这条完整业务链。不管你是拿它做毕业设计,还是想给学校信息化处交付一套能真正用得起来的系统,这篇内容都值得你从头到尾看完。

我会把系统拆分到数据库表设计、后端业务事务、前端权限路由、部署调试这几个层面,把当初踩过的坑和优化点一并讲清楚,尽量让初学者也能照着自己的项目复现。

1. 高校学科竞赛平台的核心价值与场景拆解

1.1 学科竞赛管理到底卡在哪里

我们学校之前办校级程序设计竞赛,报名用在线表格,提交作品用QQ群文件,评委打分靠Excel汇总,最后公示成绩又是另一套通知渠道。整个过程信息散落,学生找不到截止时间,指导老师看不到队伍进度,管理员统计获奖数据要花整整两天。学科竞赛平台要解决的,就是让竞赛从发布报名、组建团队、提交作品到成绩核定的全流程都在一个系统里闭环。

更深一层,这套系统还要考虑学校多赛事并行的场景。校级竞赛一年能办几十场,不同赛事有不同赛制(个人赛、团体赛)、不同评委、不同评分维度,如果平台不能把这些灵活配置化,最后就是一锤子买卖,办完一场就废了。所以系统的核心价值不只是“报名线上化”,而是赛事流程的标准化和可配置化。

1.2 系统角色与功能地图

从实际使用对象来看,系统需要拆出四种角色,每一个角色的操作面和数据权限都不一样:

  • 学生用户:浏览竞赛公告、查看赛程安排、创建或加入团队、在线提交报名信息、上传作品材料、查询个人成绩与获奖记录。
  • 指导教师:审核自己名下学生的报名资格,查看团队作品,给出推荐意见,辅导记录留痕。
  • 评委/评审专家:按分配到的赛项查看参赛作品,在线打分并填写评审意见,支持多轮评分汇总。
  • 系统管理员:维护竞赛基础数据(学院、专业、赛制类型)、创建赛事和赛项、配置评分模板、管理用户账号、发布公告、导出报名数据和成绩报表。

功能模块对应的就是一个管理后台加一个学生前台。前台给学生看赛程、报名、传作品、查成绩;后台给管理员和评委做配置和评审。这两个端共用一套后端接口,只是路由权限做了区分。

1.3 为什么这套技术栈是合理选择

SpringBoot负责后端接口服务,MyBatis负责数据库交互层,MySQL存业务数据,Vue做前端界面。这套组合在高校和中小型企业内部系统里出镜率极高,原因有三个:

第一,SpringBoot简化了SSM时代的配置地狱,内嵌Tomcat让项目能够直接打成jar包运行,很适合部署在学校机房或云服务器上。第二,MyBatis的SQL手写能力在复杂报表统计、多表关联查询场景下比JPA更直观可控,竞赛平台恰好有大量跨表统计。第三,Vue的前后端分离架构让前端展示逻辑和后端数据逻辑各司其职,遇到页面调整不用重新动Java代码。

用这套组合还有一个隐性好处:招人好招。高校的技术维护人员、参与项目的学生,SpringBoot和Vue基本都是上手最快的技术栈,遇到问题社区资料也最多,后期交接维护的阻力最小。

2. 开发环境准备与项目初始化

2.1 版本选型:SpringBoot 2.7还是3.x

这里必须说一个很多人上来就踩的坑。SpringBoot 3.x发布后,很多人新建项目直接选择最新版,结果连MyBatis依赖都装不上,或者启动直接报javax.servlet不存在。原因是SpringBoot 3基于Jakarta EE命名空间,老一批mybatis-spring-boot-starter还是基于javax的,兼容性一团乱。

我在这套项目里用的是SpringBoot 2.7.18,这是2.x系列的最后一个维护版本,稳定性和兼容性都经过长时间验证。MyBatis使用mybatis-spring-boot-starter 2.3.2版本,配合MySQL驱动mysql-connector-java 8.0.33,这是经过大量生产环境验证的组合。

JDK建议使用1.8或11。如果本机已经装了JDK 17,跑SpringBoot 2.7也没有问题,但不要把JDK版本和SpringBoot 3强行绑定,除非你有把握处理javax到jakarta的迁移。

2.2 Maven多模块还是单模块

高校竞赛平台这类业务系统,我的建议是单模块就够了,不要为了炫技拆成多模块。多模块适合需要复用公共代码的微服务或组件库场景,而这个系统的代码量大概在3000到5000行之间,单模块结构清晰,打jar包简单,调试链路也短。

当然,包结构要规划好。按照controller、service、mapper、entity、config、common、utils来分包,其中common放统一返回结果和异常处理,utils放JWT工具、文件上传工具等。这样虽然只有一个maven模块,但代码层次分明,后期扩展成多模块也容易。

2.3 初始化后端工程的正确步骤

我用IDEA新建SpringBoot项目时,通常不是直接用Spring Initializr生成,而是先创建空白Maven项目,再手动引入依赖。这样做的原因是Initializr默认生成的依赖版本经常和本地的Maven仓库不一致,生成后还需要调整。手动创建虽然多几步,但每一步都清楚自己在干什么。

pom.xml里的核心依赖如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.48</version> </dependency> </dependencies>

启动类上记得加@MapperScan注解扫描mapper接口包,不然MyBatis会找不到数据访问接口。这个注解很多人漏掉,然后报Invalid bound statement错误,其实问题不在XML,是根本没有扫描到Mapper接口。

2.4 IDEA中配置启动端口与热部署

开发阶段我习惯在application.yml里单独配置开发环境端口,而不是直接改默认的8080。

server: port: 8081

为什么用8081?因为学校服务器上往往还跑着其他Tomcat应用,8080很容易被占用。用8081做本地开发端口,上线时再通过启动参数覆盖,例如java -jar app.jar --server.port=8080,这样灵活度最高。

IDEA里配置启动端口不需要改任何额外设置,SpringBoot应用启动时读取的就是application.yml里的server.port。注意如果你的项目里同时存在application.yml和application.properties,IDEA默认以properties优先,两个文件都配置了不同端口时会以properties里的为准,这个细节容易让人困惑。保持一套配置方式,不要混用。

热部署方面,引入spring-boot-devtools依赖后,IDEA还需要开启Build project automatically,并在Registry里勾选compiler.automake.allow.when.app.running,否则改代码不会自动重启。这个环节很容易被忽略,导致很多人觉得devtools没用。

3. 数据库设计:从业务需求到表结构落地

3.1 核心业务表拆解

竞赛平台的核心数据表,我梳理下来一共八张,每一张都有明确的业务定位:

  • t_user:用户表,存储学生、教师、评委、管理员的账号信息和基本资料,用role_type字段区分角色,不建议把角色拆成单独表,会因为角色固定而增加没必要的关联查询。
  • t_competition:竞赛表,存储校级或院级竞赛的总体信息,包括竞赛名称、主办单位、报名起止时间、竞赛状态(未开始、报名中、评审中、已结束)。
  • t_event:赛项表,一个竞赛可以包含多个赛项,比如“程序设计竞赛”下面拆出“算法组”“Web应用组”“数据库组”,每个赛项单独设置报名限额和评分规则。
  • t_team:团队表,记录参赛团队的基本信息,包括团队名称、所属赛项、队长ID、指导老师ID、报名状态。
  • t_team_member:团队成员表,一个团队对应多个成员,这里用单独的关联表,避免在t_team里存冗余字段。
  • t_work:作品表,存储团队提交的作品文件路径、作品说明、提交时间,一个团队可以多次提交,但只保留最新版本(或者做版本管理)。
  • t_score:评分表,记录评委给某个团队作品的打分明细,包括分数、评语、评分时间,一个作品对应多条评分记录。
  • t_notice:公告表,发布各类通知消息,公告类型区分竞赛动态和系统通知。

3.2 状态字段为什么用int不用字符串

这是我在设计表结构时特别注意的一点。很多初学者喜欢把状态字段设为varchar,直接存“已报名”、“审核中”这种中文,看着直观,但后续扩展和统计很痛苦。状态字段我用tinyint,通过0、1、2、3来表示不同状态,Java代码里再通过枚举类做映射。

以报名状态为例:

public enum TeamStatus { DRAFT(0, "草稿"), SUBMITTED(1, "已提交待审核"), PASSED(2, "审核通过"), REJECTED(3, "审核不通过"); }

这样做的好处有三个:一是存储空间小,查询效率高;二是状态流转更可控,Java枚举能约束取值范围;三是对接前端下拉框时,前端只需要循环枚举生成选项,改中文名不用改数据库。缺点是写SQL时要记住数字含义,所以项目中的注释和枚举类要写全,别偷懒。

3.3 建库建表SQL与初始化数据

数据库名我建议用competition_db,字符集必须用utf8mb4而不是utf8。utf8在MySQL里最多只能存3字节的字符,遇到emoji表情或者生僻字会直接报错,utf8mb4是utf8的超集,兼容性最好。

下面是核心的建库语句和竞赛表结构:

CREATE DATABASE competition_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE t_competition ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', competition_name VARCHAR(100) NOT NULL COMMENT '竞赛名称', organizer VARCHAR(100) COMMENT '主办单位', start_time DATETIME COMMENT '报名开始时间', end_time DATETIME COMMENT '报名截止时间', status TINYINT DEFAULT 0 COMMENT '状态 0未开始 1报名中 2评审中 3已结束', description TEXT COMMENT '竞赛描述', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '竞赛信息表';

初始化数据方面,我会写一个schema.sql或者直接在Navicat里执行一段INSERT脚本,给管理员账号、测试学生账号、测试教师账号各建一个。注意密码不能明文存储,这里用MD5还是BCrypt取决于项目规模,竞赛平台我建议用BCrypt加密,SpringSecurity的BCryptPasswordEncoder可以直接用,比MD5加盐更方便。

4. 后端核心模块实现与难点解析

4.1 统一返回结果与控制层设计

前后端分离项目中,统一返回结构是必须的。我定义了一个Result类,所有接口返回都走这个包装,前端拿到数据后只需统一解析一次。

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

控制层的写法也有讲究。不要在Controller里堆业务逻辑,Controller的作用就是接收参数、调用Service、返回Result。判断参数是否合法的工作交给@Valid注解,配合VO对象做字段校验。

@PostMapping("/team") public Result<?> createTeam(@RequestBody @Valid TeamVO teamVO) { teamService.createTeam(teamVO); return Result.success(null); }

4.2 MyBatis初始化原理与XML映射要点

MyBatis启动时,会通过XMLConfigBuilder读取mybatis-config.xml或SpringBoot配置中的相关属性,解析数据源、事务、映射器注册中心,然后为每个Mapper接口生成代理实现。很多新人遇到Mapper方法找不到或者SQL写错了不报错、运行时才报错的情况,就是因为对MyBatis初始化流程不够熟悉。它不像JPA那样启动时做全量校验,XML里的SQL语法错误要等到首次调用才会暴露。

所以我在开发中养成了一个习惯:写完一个Mapper方法,立刻在测试类里调用一次,而不是等联调阶段才暴露问题。

MyBatis的XML映射有几个关键点。resultMap用于处理数据库字段和Java实体属性不一致的问题,比如数据库里的create_time映射为createTime,如果开启了驼峰映射,mybatis.configuration.map-underscore-to-camel-case=true就不用写resultMap了。动态SQL用 标签拼接查询条件,但要注意if标签内不能出现多余的逗号,这是新手最容易犯的错。

下面这段是一个竞赛列表的条件分页查询:

<select id="selectCompetitionList" resultType="com.example.entity.Competition"> SELECT id, competition_name, organizer, start_time, end_time, status FROM t_competition <where> <if test="status != null"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND competition_name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

这里有个细节:LIMIT的offset和pageSize如果直接写在SQL里,需要从PageVO对象里手动计算offset = (pageNum - 1) * pageSize。我倾向于不使用PageHelper插件,因为对新手来说,理解分页逻辑本身比引入插件更重要,而且手写LIMIT在数据量不大的情况下性能完全足够。

4.3 登录鉴权与拦截器实现

登录鉴权我选择JWT方案,而不是Session。原因是前后端分离后,前端可能是浏览器也可能是移动端,Session跨域处理麻烦,而JWT是无状态的,后端不用保存登录状态,前端请求时在请求头带上token即可。

用户登录接口的逻辑是这样的:接收用户名和密码,通过Mapper查询用户,用BCrypt校验密码,校验通过后生成JWT令牌,JWT中存入userId、username、roleType,设置过期时间为24小时。然后返回给前端,前端存到localStorage,后续所有请求通过拦截器携带。

后端拦截器要做的就是从请求头里取出token,解析校验,然后把用户信息放到ThreadLocal里,方便Controller直接获取当前登录用户。

拦截器注册代码如下:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null) { response.setStatus(401); return false; } try { Claims claims = JwtUtil.parseToken(token); UserContext.setUserId(claims.get("userId", Long.class)); UserContext.setRoleType(claims.get("roleType", Integer.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }

注意拦截器放行规则。登录接口、注册接口、公告查询接口要放行,其余接口都要校验token。如果使用WebMvcConfigurer注册拦截器,还要记得放行静态资源路径,否则前端请求图片或文件也会被拦下来。

4.4 事务控制与数据一致性保障

竞赛报名过程中有一个最容易出问题的环节:创建团队时,要先插入团队表,再插入团队成员表,同时还要更新赛项的已报名人数。这三个步骤任何一个失败,都会留下脏数据。这时候就必须用事务。

在SpringBoot中使用事务极其简单,在Service方法上加@Transactional注解即可。

@Transactional(rollbackFor = Exception.class) public void createTeam(TeamVO teamVO) { Team team = new Team(); team.setTeamName(teamVO.getTeamName()); team.setEventId(teamVO.getEventId()); team.setLeaderId(UserContext.getUserId()); team.setStatus(TeamStatus.DRAFT.getCode()); teamMapper.insertTeam(team); for (Long memberId : teamVO.getMemberIds()) { teamMemberMapper.insertTeamMember(team.getId(), memberId); } eventMapper.increaseRegisteredCount(teamVO.getEventId()); }

这里有个重点:rollbackFor必须写成Exception.class,因为Spring默认只对RuntimeException回滚,而像IOException这种受检异常不会触发回滚。如果不显式指定,遇到业务异常时数据依然会提交,这是个非常隐蔽的事务坑。

数据一致性还有一个场景是防止重复报名。用户在短时间内点击两次提交按钮,可能产生两个相同的团队记录。我在实现时用数据库层面的唯一索引兜底,team表上建一个(event_id, leader_id)的唯一索引,即使应用层因为并发没拦住,数据库也会直接报错拒绝第二次插入。

5. 前端页面的搭建与关键功能实现

5.1 Vue工程初始化与路由权限

前端工程使用Vue 3 + Vite + Element Plus的组合,这个组合在2023年之后已经成为主流。创建命令如下:

npm create vite@latest competition-web -- --template vue

进入项目后安装依赖:

npm install vue-router@4 element-plus axios pinia

路由权限是前端实现的重点。我的思路是:定义完整的路由表,但根据用户角色动态过滤可访问的路由表,通过router.beforeEach全局前置守卫判断用户登录状态和角色权限。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next('/login'); } else if (token && to.path === '/login') { next('/'); } else { next(); } });

管理端和前台通过不同布局区分。管理端的路由挂在Layout组件下,侧边栏根据菜单配置生成,这种动态路由的写法在Vue3中要用router.addRoute动态添加,而不是预先全部注册。

5.2 Axios封装与接口联调技巧

Axios封装是前端工程化的基础操作。我会在src/utils/request.js里创建统一的axios实例,配置baseURL、超时时间、请求拦截器、响应拦截器。

import axios from 'axios'; import { ElMessage } from 'element-plus'; 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 => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } ElMessage.error('服务器开小差了,请稍后重试'); return Promise.reject(error); } );

联调阶段最常遇到的就是跨域问题。我的做法是:开发环境用Vite的代理配置,而不是在后端加@CrossOrigin。Vite代理配置在vite.config.js里:

server: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }

这样前端请求/api/competition/list,在开发环境下自动转发到后端的/competition/list,没有跨域问题。上线后Nginx再配置一层反向代理,保持前端的请求路径完全不变。

5.3 竞赛报名表单与评审后台实现

报名表单是整个前台最复杂的交互。一个团队报名需要填写团队名称、选择赛项、添加成员(从同学列表里选择)、上传指导老师同意书、填写作品简介。表单用Element Plus的el-form做校验,其中成员选择用el-select的多选模式,支持远程搜索用户。

评审后台需要展示当前登录评委名下的待评审作品列表,每条作品展示团队信息、作品附件、打分输入框和评语文本框。这里有个交互细节:评委可能评到一半要去处理别的事,所以打分状态需要保存草稿。我的做法是增加一个状态字段,点击“暂存”只更新数据库,点击“提交评分”才正式落定且不可修改。

5.4 视频文件预览与m3u8播放方案

竞赛平台经常遇到作品包含视频的情况,比如微课竞赛、短视频创作赛。如果视频体积较大,建议上传转码后的m3u8切片文件而不是直接传mp4。m3u8的优势是可以边下边播,用户在浏览器里不用等整个文件加载完成。

前端播放m3u8可以使用hls.js库,不需要额外安装任何浏览器插件:

npm install hls.js
import Hls from 'hls.js'; const video = document.getElementById('player'); if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource('http://your-server.com/videos/demo.m3u8'); hls.attachMedia(video); }

如果浏览器原生支持HLS(比如Safari),则直接设置video的src为m3u8地址即可。hls.js的兼容性已经很好,主流的Chrome、Edge、Firefox都能通过它播放m3u8流。

后端存储方面,如果视频文件较多,不建议存MySQL的BLOB字段,应该走单独的存储服务。MinIO是一个兼容S3协议的对象存储方案,SpringBoot整合MinIO的依赖是io.minio:minio:8.5.7,通过Java SDK上传文件,返回文件的访问URL,数据库里只存URL字符串。

6. 整套系统的编译打包与部署上线

6.1 Maven生命周期与打包命令

完成开发后,部署的第一步是打包后端项目。在IDEA右侧Maven面板里,先执行clean清理target目录,再执行package打包。如果你更习惯命令行,也可以直接用:

mvn clean package -DskipTests

-DskipTests很关键。如果不跳过测试,Maven会执行src/test目录下的单元测试类,而测试类中通常包含了数据库连接信息,如果本地数据库没启动,打包过程会被测试失败阻断。我见过太多人在这卡住,其实只要跳过测试就好了,测试在开发阶段单独执行。

打包完成后,target目录下会生成一个jar文件。SpringBoot默认的打包方式是可执行jar包,不需要外部Tomcat,直接用java命令就能启动。

6.2 前端构建与Nginx反代

前端构建更简单:

npm run build

构建完成后,dist目录里就是纯静态文件,包括HTML、CSS、JavaScript。把这个目录上传到服务器,用Nginx指向它就行。

Nginx配置核心是静态资源服务和/api反向代理。前端请求/api/xxx时,Nginx把请求转发给SpringBoot服务:

server { listen 80; server_name competition.example.com; root /opt/competition/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里要注意proxy_pass后面的斜杠。写成http://127.0.0.1:8081/时,Nginx会把请求路径中/api的前缀去掉,再拼接到后端,也就是前端请求/api/competition/list,后端收到的是/competition/list。如果写成http://127.0.0.1:8081(没有末尾斜杠),后端会收到完整的/api/competition/list路径,可能导致404。

6.3 服务器目录规划与启动脚本

我部署这套系统的服务器目录结构如下:

/opt/competition/ ├── dist/ # 前端静态文件 ├── app.jar # 后端SpringBoot包 ├── uploads/ # 上传的文件(图片、作品附件) ├── logs/ # 运行日志 └── start.sh # 启动脚本

start.sh脚本内容:

#!/bin/bash APP_NAME=app.jar LOG_FILE=/opt/competition/logs/app.log nohup java -jar /opt/competition/$APP_NAME \ --server.port=8081 \ --spring.profiles.active=prod \ --spring.servlet.multipart.max-file-size=200MB \ --spring.servlet.multipart.max-request-size=200MB \ > /opt/competition/logs/startup.log 2>&1 & echo "Application started, PID: $!" echo $! > /opt/competition/app.pid

多进程路径和文件上传大小限制放在启动参数里,比写在application.yml里更灵活。这样同一套jar包,在开发、测试、生产环境只要通过启动参数区分配置即可,不需要维护多份配置文件。

7. 高频问题排查与避坑清单

7.1 数据库SSL连接报错

MySQL 8.0默认开启了SSL,如果连接字符串没有正确处理,运行时会报java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed或者SSL连接相关的警告。

解决办法是在jdbc连接串上显式关闭SSL,并允许公钥检索:

spring: datasource: url: jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

7.2 端口占用与启动失败

端口被占用是最常见的启动失败原因。Linux下先用命令确认端口占用情况:

netstat -tlnp | grep 8081

如果是自己之前启动的进程,用PID找到进程并杀掉:kill -9 PID。如果是其他服务占用,最省事的做法是换端口。IDEA里编辑SpringBoot启动配置,在Environment变量的VM options里加-Dserver.port=8082,或在Program arguments里加--server.port=8082,两种方式任选一种。

7.3 MyBatis不打印SQL与映射失效

调试阶段看不到SQL是很多人的痛点。在application.yml里加一个配置即可:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这个配置会让MyBatis在控制台打印完整的SQL语句和参数值,定位SQL拼接问题非常好用。生产环境记得注释掉,否则日志会刷屏并且泄漏数据细节。

另外一个高频问题是“Invalid bound statement (not found)”。出现这个错误时,先检查三件事:Mapper接口和XML文件是否在同一个包路径下(Maven多模块时尤其容易出错);XML文件的namespace是否和Mapper接口全限定名一致;application.yml里是否配置了mybatis.mapper-locations。前两个问题检查完,90%的这类错误都能解决。

7.4 跨域与Session失效问题

开发环境跨域走Vite代理,生产环境走Nginx反代,基本都能规避跨域。如果你非要在后端直接开启跨域,使用CorsFilter而不是@CrossOrigin注解,因为注解只能对单个Controller生效,Filter是全局生效的:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

使用JWT后基本不存在Session失效问题,但要注意token过期后前端要能感知。我在响应拦截器里对401状态做了统一处理,跳转到登录页并清理本地存储。如果没有这个逻辑,用户token过期后只能看到接口报错,体验很差。

我自己实际跑这套系统时最大的体会是:竞赛平台这种业务系统,难点不在某个单独的技术点,而在状态流转和角色权限的边界。团队从报名到审核到提交作品到评分,每个环节的数据库更新都要想清楚是哪个角色在什么状态下触发的,不然就会出现学生已经把作品交了、后台还显示待提交的尴尬情况。这些状态机逻辑,建议在编码之前先画一张状态流转图贴在手边,比边写边想靠谱得多。

文章评论(0)

返回列表