SpringBoot+Vue高校学科竞赛平台:从数据库设计到部署上线实战
发布时间:2026/10/1 13:54:15来源:尧图网络
做高校竞赛管理这块系统市面上能找到的完整源码不少但大多是教学级Demo要么报名流程跑不通要么评审环节干巴巴就一个增删改查。今天分享的这套基于SpringBootVueMySQLMyBatis的高校学科竞赛平台是我在实际项目中从零梳理并落地过的完整实现覆盖了竞赛发布、在线组队报名、材料审核、评委评分、成绩公示、证书管理这条完整业务链。不管你是拿它做毕业设计还是想给学校信息化处交付一套能真正用得起来的系统这篇内容都值得你从头到尾看完。我会把系统拆分到数据库表设计、后端业务事务、前端权限路由、部署调试这几个层面把当初踩过的坑和优化点一并讲清楚尽量让初学者也能照着自己的项目复现。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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.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.port8080这样灵活度最高。IDEA里配置启动端口不需要改任何额外设置SpringBoot应用启动时读取的就是application.yml里的server.port。注意如果你的项目里同时存在application.yml和application.propertiesIDEA默认以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 ) ENGINEInnoDB COMMENT 竞赛信息表;初始化数据方面我会写一个schema.sql或者直接在Navicat里执行一段INSERT脚本给管理员账号、测试学生账号、测试教师账号各建一个。注意密码不能明文存储这里用MD5还是BCrypt取决于项目规模竞赛平台我建议用BCrypt加密SpringSecurity的BCryptPasswordEncoder可以直接用比MD5加盐更方便。4. 后端核心模块实现与难点解析4.1 统一返回结果与控制层设计前后端分离项目中统一返回结构是必须的。我定义了一个Result类所有接口返回都走这个包装前端拿到数据后只需统一解析一次。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT 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-casetrue就不用写resultMap了。动态SQL用 标签拼接查询条件但要注意if标签内不能出现多余的逗号这是新手最容易犯的错。下面这段是一个竞赛列表的条件分页查询select idselectCompetitionList resultTypecom.example.entity.Competition SELECT id, competition_name, organizer, start_time, end_time, status FROM t_competition where if teststatus ! null AND status #{status} /if if testkeyword ! 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 vitelatest competition-web -- --template vue进入项目后安装依赖npm install vue-router4 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.jsimport 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_NAMEapp.jar LOG_FILE/opt/competition/logs/app.log nohup java -jar /opt/competition/$APP_NAME \ --server.port8081 \ --spring.profiles.activeprod \ --spring.servlet.multipart.max-file-size200MB \ --spring.servlet.multipart.max-request-size200MB \ /opt/competition/logs/startup.log 21 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?useUnicodetruecharacterEncodingutf8useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai7.2 端口占用与启动失败端口被占用是最常见的启动失败原因。Linux下先用命令确认端口占用情况netstat -tlnp | grep 8081如果是自己之前启动的进程用PID找到进程并杀掉kill -9 PID。如果是其他服务占用最省事的做法是换端口。IDEA里编辑SpringBoot启动配置在Environment变量的VM options里加-Dserver.port8082或在Program arguments里加--server.port8082两种方式任选一种。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)
网站建设高端定制企业官网