做了几年高校信息化项目,类似“竞赛管理系统”这种需求见过不少,也踩过不少坑。现在很多学校还在用Excel统计报名、邮件发通知、人工核对获奖名单,偶尔还会遇到错过报名截止时间、成绩统计对不上号的尴尬局面。所以看到“基于SpringBoot+Vue的高校竞赛管理系统,配套MyBatis+MySQL”这个标题,我第一反应是:这不就是高校教务/团委信息化里最典型、也最能拿得出手的一套全栈练手项目嘛。它不只是一串技术名词的堆叠,更是一个覆盖权限设计、流程审批、数据统计、前后端联调的真实业务系统。
这套系统适合谁?一类是计算机相关专业的学生,准备毕业设计、课程设计或者找工作简历上的项目经验,技术栈主流,面试时能聊的点非常多;另一类是高校信息化部门的开发人员,想用更轻量的方式替代纸质流程,或者在此基础上做二次开发。全文不会只停留在“能跑就行”的层面,我会把项目拆开揉碎,从需求分析讲到底层表结构,再到前后端的关键实现和常见坑,尽量做到拿过去就能改、能部署、能演示。
1. 项目定位与核心需求拆解
1.1 竞赛管理这件事,到底难在哪
先别急着聊技术,我习惯写代码前先把业务捋清楚。高校竞赛管理,表面上就是“发布竞赛→学生报名→老师审核→举办比赛→录入成绩→发放证书”,但真实场景远没那么简单。
第一个痛点是角色多。学生要能看到全部竞赛、在线报名、查看自己的获奖记录;指导教师要能审核自己指导的学生、录入成绩、导出名单;二级学院管理员要管理本院师生、查看本院竞赛参与情况;校级管理员则要维护全校的竞赛分类、比赛通知、评委分配、最终审核、数据统计。每个角色看到的界面不同,能操作的范围不同,背后就是权限模型的设计问题。
第二个痛点是数据流转复杂。一次竞赛从“通知报名”到“最终归档”,状态可能有十几种:未开始、报名中、报名截止、初审中、审核通过、评审中、已结束、已公示、已归档。如果只是给业务表加一个status字段,后面统计时SQL会写得非常痛苦。
第三个痛点是报表需求多且杂。学校层面每年需要统计“学科竞赛获奖情况”“学生参与人次”“教师指导项目数”,而且经常要求按学院、按年度、按竞赛级别(国家级/省级/校级)交叉筛选。如果数据库表设计得不够规范,这些报表靠程序里硬编码逻辑拼接,后期会变成维护噩梦。
所以这套系统的核心不是“CRUD写得有多花”,而是:用一套合理的数据模型,把竞赛流程和权限边界理顺,再通过前端界面让不同角色都能高效操作。SpringBoot + Vue 这个组合正好能覆盖两端需求,MyBatis控制SQL灵活性,MySQL作为业务数据库,整个技术栈非常契合这类中后台管理系统。
1.2 技术栈选型:为什么是这四件套
我先说SpringBoot。高校内部系统通常不是高并发互联网应用,QPS能到几百就谢天谢地了,SpringBoot足够稳,生态成熟,最重要的是开发效率高。它内置Tomcat,打包后一个jar直接扔服务器上java -jar就能跑,对高校机房或者实验室服务器都很友好。另外Spring Security或者结合JWT做登录认证,社区文档非常多,调试起来不太会卡壳。
Vue这边,现在普遍用的是Vue 3 + Element Plus + Vite。Element Plus的表格、表单、弹窗、树形控件基本覆盖了后台管理90%的界面需求。竞品系统不需要酷炫的动效,需要的是信息密度高、操作路径短、筛选查询顺手,Vue + Element Plus正好是干这个的。
MyBatis在这个项目里的定位非常明确:复杂SQL需要手动控制。竞赛管理系统的统计报表部分,经常需要多表关联、分组聚合、条件动态拼接,用JPA这种ORM有时候会生成一坨让人看不懂的SQL,而MyBatis可以把SQL和Java逻辑分离,写起来更直白,调优也容易。再加上MyBatis-Plus的BaseMapper和LambdaQueryWrapper,日常单表CRUD也不用自己手写XML,效率很高。
MySQL就不用多说了,开源免费、部署简单,高校场景的数据量(几万条用户、几千条竞赛记录、几十万条报名记录)完全够用。哪怕以后要上等保、做备份,MySQL的周边工具也足够成熟。
2. 数据库设计与核心表结构解析
2.1 从业务对象到表清单
数据库设计是这类系统的地基。我建议先梳理出核心业务对象,再确定表之间的关系,最后再设计字段。竞赛管理系统至少需要下面几张表:
- 用户表(user):账号、密码、姓名、角色、所属学院
- 学院表(college):学院编码、名称、排序
- 竞赛表(competition):竞赛名称、级别、类型、状态、报名开始/结束时间、比赛时间、赛事介绍
- 报名表(registration):学生报名记录,关联竞赛、学生、指导教师、审核状态
- 获奖表(award):竞赛获奖结果,关联报名表或学生,包含奖项级别、证书编号
- 通知公告表(notice):后台发布的比赛通知
- 角色权限表(role/permission):也可以简化为在用户表里直接放role字段,但更规范的是独立表
不要一上来就建十几张表,先把核心链路跑通,再根据需求补充表。系统不是表越多越好,但每个关键业务节点都得有对应的数据载体。
2.2 用户与角色:RBAC还是简化版
很多初学者把角色直接写成user表里的一个字段,比如role=0学生、1老师、2管理员。这种思路在demo里没问题,但到了真实项目就会卡壳:一个老师可能既是学院管理员,又是竞赛评委,还是某个竞赛的指导老师。简单角色字段根本表达不了。
我建议在核心表之外增加一张“用户-角色分配表”和“角色表”,把用户的角色做成多对多。Spring Security里的权限判断可以基于角色标识符来做,前端再根据当前角色动态渲染菜单。如果确实不想搞太复杂,至少也要支持“一个用户拥有多个角色标识”的数据结构,而不是用一个单值字段硬扛。
这里给一套比较实用的简单RBAC设计:
- 用户表只存基础信息,不存角色
- 角色表(role):存储角色编码和名称,例如STUDENT、TEACHER、COLLEGE_ADMIN、ADMIN
- 用户角色关联表(user_role):一个用户可关联多个角色
登录后接口返回角色编码列表,前端用这个列表控制页面访问和按钮显示,后端在Controller接口上用@PreAuthorize注解做二次拦截。
2.3 竞赛状态字段设计
竞赛表的status字段需要定义成可扩展的编码类型,我常用的是整数状态,配合枚举类维护:
- 0:草稿
- 1:已发布,报名中
- 2:报名截止
- 3:初审中
- 4:终审/评审中
- 5:结果公示中
- 6:已归档
这里有个小技巧:不要把审核状态和竞赛状态混在一起。竞赛状态描述的是竞赛整个生命周期的当前阶段;而报名记录有自己的审核状态(待审核、通过、驳回、撤销)。两条状态线各自独立,后续做统计、做消息通知时逻辑会很清楚。
2.4 MyBatis-Plus的字段填充
用MyBatis-Plus做自动填充能省很多事。比如create_time、update_time字段可以通过@TableField(fill = FieldFill.INSERT)和MetaObjectHandler自动填充,不用每个Service里手动set值。删除逻辑上,建议给核心表加一个deleted字段,统一逻辑删除,避免物理删除导致历史关联数据断裂。报名记录、成绩记录这种流水型数据尤其不能硬删,一旦删了,后面审计、补发证书都会出问题。
下面是我常用的一张报名表的核心DDL,供参考:
CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL COMMENT '竞赛ID', student_id BIGINT NOT NULL COMMENT '学生用户ID', teacher_id BIGINT DEFAULT NULL COMMENT '指导教师用户ID', team_name VARCHAR(100) DEFAULT NULL COMMENT '团队名称/个人', status TINYINT DEFAULT 0 COMMENT '审核状态 0待审核 1通过 2驳回 3已撤销', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, UNIQUE KEY uk_comp_student (competition_id, student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='竞赛报名表';这里加了一个唯一索引竞赛ID+学生ID,保证同一个学生不能重复报名同一竞赛。业务上还要考虑团队参赛,那时候报名主体就是团队而不是个人,需要再抽团队表,这里不做展开。
3. 后端核心实现:从登录鉴权到业务闭环
3.1 登录鉴权与权限拦截
后端第一个要解决的是登录问题。我们用的是SpringBoot + Spring Security + JWT组合。流程是用户登录时校验账号密码,成功后生成JWT返回前端;前端把token存到localStorage,每次请求在Header里携带Authorization: Bearer token;后端通过OncePerRequestFilter解析token,把用户信息放入SecurityContext。
这里有几个容易踩的坑:
- JWT过期时间不要设置太长,后台管理系统建议2小时左右。之前见过设置为7天的,学生账号在公共机房忘记退出,后面麻烦不断。
- 密码加密必须用BCrypt,不要用MD5。MD5撞库太容易了,高校学生账号很多是身份证号或者学号当用户名,不安全。
- 用户被禁用时,哪怕token没过期,也要能在过滤器里查到用户状态并拦截。所以token里别只存用户ID,最好把用户状态也缓存到Redis里,每次请求校验一下。如果不想引入Redis,也可以在过滤器中每次查询用户表,但性能有点浪费。
登录接口的简化代码框架:
public class LoginController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenProvider tokenProvider; @PostMapping("/api/auth/login") public Result login(@RequestBody LoginRequest request) { String username = request.getUsername(); String password = request.getPassword(); // Spring Security 认证 Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) ); // 生成token String token = tokenProvider.generateToken(authentication); return Result.ok(token); } }Controller层接口一定要加上权限注解,别只靠前端隐藏按钮。例如管理员删除竞赛的接口,可以这样标注:
@PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/competition/{id}") public Result deleteCompetition(@PathVariable Long id) { ... }3.2 竞赛发布与报名流程
竞赛发布功能的后端逻辑相对简单,主要是校验竞赛状态和时间参数。比较关键的点是:报名开始和结束时间的校验放到Service层,用LocalDateTime判断,不要用前端传的字符串直接比较。
比如我要发布一个竞赛,Controller接收DTO,DTO上用@NotBlank、@NotNull标注必填字段,然后Service里再做业务校验:开始时间不能晚于结束时间,报名截止时间不能早于当前时间,竞赛状态必须是草稿才能修改。如果校验不通过,抛出一个自定义业务异常,由全局异常处理器统一返回错误信息。
报名流程需要注意并发问题。有些竞赛名额有限,比如“程序设计大赛校级选拔赛”限制500人,一旦报满就不能再报。这种场景下用简单的update语句配合条件更新可以避免超卖:
UPDATE competition SET registered_count = registered_count + 1 WHERE id = #{competitionId} AND status = 1 AND registered_count < max_people;通过受影响行数判断是否报名成功,比先查后更安全。MySQL默认隔离级别下,简单的原子更新不会产生太多并发问题。当然如果报名人数只有几千,这种写法足够稳定。
报名完成后建议发送站内通知。不需要真的接短信或者邮件,只要在通知表里插入一条记录,学生登录系统后在消息中心能看到即可。这个小功能能显著提升体验,学生不用每天开着系统刷报名结果。
3.3 成绩录入与结果统计
成绩录入最麻烦的是“批量录入”。一场比赛可能有几百个获奖学生,管理员不可能一个一个点编辑。后来我们做了一版Excel批量导入,后端用EasyExcel解析上传的表格,校验学生学号是否存在、是否报名过该竞赛、奖项是否在预设范围内,然后批量写入获奖表和更新报名状态。
批量导入的异常处理要细心。不能因为表格里有一行数据错误就整批回滚,最好逐行校验,把错误行号和信息汇总,最后返回给前端一个“部分成功”的提示。用户看到错误文件能照着改,而不是只看到“导入失败”四个字。
统计报表部分,我习惯在SQL里直接算好,再映射成VO。例如统计某个学院获奖情况:
SELECT c.college_name, COUNT(CASE WHEN a.level = '国家一等奖' THEN 1 END) AS national_first_count, COUNT(CASE WHEN a.level = '省部级一等奖' THEN 1 END) AS province_first_count FROM award a JOIN user u ON a.student_id = u.id JOIN college c ON u.college_id = c.id WHERE a.competition_id = #{competitionId} GROUP BY c.id, c.college_name用CASE WHEN做行转列,比在Java里循环统计高效得多。MyBatis查询结果直接映射给一个带有statistics字段的VO,前端渲染成表格或者图表都方便。
3.4 MyBatis-Plus使用与SQL优化心得
MyBatis-Plus的LambdaQueryWrapper确实好用,但别把所有操作都寄托在它身上。多表关联查询、子查询、复杂聚合,还是老老实实写XML里的SQL,用@Select注解不如XML直观,XML还方便统一管理。
动态SQL要小心模糊查询的模糊匹配。如果直接使用前端传入的字符串,不做转义,可能出现下划线通配符问题。建议用CONCAT包装:
WHERE name LIKE CONCAT('%', #{keyword}, '%')索引优化方面,报名表的主要查询条件是competition_id、student_id、status,所以这3个字段可以做联合索引。竞赛表常用的查询条件是status和publish_time,分别建立普通索引。数据量不大时,索引别建太多,避免写入性能下降。
4. 前端Vue实现:页面架构与交互设计
4.1 动态路由与菜单权限
前端第一个难题也是权限。网上很多项目写死了路由表,所有路由打包在代码里,登录后不管角色是什么,都能通过URL跳过去。这是不行的,至少要做两件事:一是侧边栏菜单根据角色动态生成,二是路由守卫拦截未登录或无权访问的链接。
我常用方案是:后端登录后返回当前用户的菜单列表,前端根据菜单列表动态构造路由,或者前端维护一套全量路由表,再按角色标识筛选出可见路由。两者各有优劣,前者灵活但稍复杂,后者实现简单、维护直观。对于高校竞赛管理系统,角色类型固定,用筛选方式就够了。
const allRoutes = [ { path: '/dashboard', component: Dashboard, meta: { roles: ['ADMIN', 'COLLEGE_ADMIN', 'TEACHER', 'STUDENT'] } }, { path: '/competition/list', component: CompetitionList, meta: { roles: ['ADMIN', 'COLLEGE_ADMIN', 'TEACHER', 'STUDENT'] } }, { path: '/competition/review', component: CompetitionReview, meta: { roles: ['ADMIN', 'COLLEGE_ADMIN'] } }, { path: '/award/import', component: AwardImport, meta: { roles: ['ADMIN'] } } ];Vue Router的beforeEach里判断用户路由是否匹配,如果不匹配就跳转404页面。按钮级别的权限判断用自定义指令v-permission,不满足条件就移除DOM元素。
4.2 报名流程的前端交互
学生端报名页面,核心是竞赛列表和竞赛详情。列表页需要支持按名称搜索、按状态筛选、按级别筛选,最好还有分页。Element Plus的el-table配合el-pagination够用了。注意分页参数要传给后端,别一次性全量渲染,数据量一大页面就会卡成幻灯片。
报名操作我是这样设计的:竞赛列表页每条数据旁边加一个“查看详情”按钮,详情页里显示竞赛说明、时间节点、报名要求,底部有一个报名按钮。点击报名按钮前,前端先调用后端接口查询当前用户是否已经报名,如果已报名则按钮置灰变成“已报名”。这样的交互比让用户填了一大堆信息后提交时才提示重复报名要友好得多。
报名表单里,学生的基本信息(学号、姓名、学院)建议后端根据登录用户自动带出,前端只让人选竞赛项目和指导老师,减少输入工作量。指导老师的选择可以做成联动的下拉框,先选学院再选老师,避免全校几千老师全部塞进一个下拉框。
4.3 管理后台的数据看板
管理员首页放几个统计卡片是很常见的需求,比如“本月报名人数”“进行中的竞赛数”“待审核报名数”“获奖学生总数”。这些数据后端可以单独写一个DashboardController,通过几条聚合SQL返回。前端用el-row和el-col布局,每个卡片放一个大数字加一个小标题,简洁直观。
图表部分,可选ECharts或者AntV G2,但如果只是柱状图、饼图,不建议引入太重的图表库。ECharts虽然功能强大,但拆包、按需加载需要配置。我一般用ECharts的按需导入,只引入Bar和Pie相关模块,避免首屏体积过大。
4.4 前端接口请求封装
用axios时,一定要做请求拦截器和响应拦截器。请求拦截器统一在Header里加入token;响应拦截器处理HTTP状态码,遇到401跳转登录页,遇到403显示“无权限”,其他错误统一用ElMessage弹出提示。
这里有个很常见的坑:后端返回的文件流(Excel下载)和JSON数据要区分处理。responseType需要动态设置,下载文件时是blob,否则前端拿到的是一堆乱码。我建议单独封装一个download方法,使用responseType: 'blob',然后根据后端返回的Content-Disposition解析文件名,再用URL.createObjectURL触发浏览器下载。
5. 部署、打包与容器化
5.1 前后端构建配置
前端开发时用Vite启动devServer,同时配置proxy代理,将/api开头的请求转发到后端的localhost:8080,避免本地开发跨域:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }生产环境前端构建后,把dist目录里的静态文件交给Nginx托管,Nginx再反向代理到后端SpringBoot进程。也可以直接把dist文件夹复制到SpringBoot的resources/static目录里,然后后端用一个路由转发规则,遇到非页面请求全部转发到index.html,这样打包成一个jar就能直接部署,对服务器资源紧张的小场景很实用。
5.2 Nginx配置注意事项
Nginx配置里,前端history路由模式必须配置try_files,否则刷新页面就会404。基本配置如下:
server { listen 80; server_name localhost; root /opt/competition-frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }另外,文件上传大小限制要在Nginx和SpringBoot两侧同时配置。SpringBoot默认单次请求最大1MB,竞赛的通知公告经常带大图或者附件,至少要配到10MB以上。Nginx里client_max_body_size也要同步修改,不然上传一半就被nginx断掉,而且前端收到的错误信息往往不够直观,排查起来很费劲。
5.3 MySQL初始化与数据备份
部署时数据库初始化有两种方式:一种是直接用HeidiSQL或Navicat导入SQL脚本,另一种是使用SpringBoot配置自动建表。项目开发阶段可以配ddl-auto: update,生产环境必须关掉,手动维护DDL变更,防止误操作覆盖数据。
高校系统最怕假期服务器断电,数据库没做备份。建议写个简单的cron脚本,每天凌晨备份一次MySQL,保留近7天备份文件。备份命令大致如下:
mysqldump -u root -p --single-transaction --routines --triggers competition_db > backup_$(date +\%Y\%m\%d).sql压缩后最好再同步到另一台机器,比如用rsync定时推送到备份服务器。哪怕学校没有专业运维,这个最简单的方案也能救命。
6. 测试用例设计:不能只测“能跑”
6.1 流程链路的完整测试
我见过很多人做项目,页面能跳转就觉得系统做完了。但这类管理系统,最怕的就是流程状态错乱。举几个必测的流程:
- 学生注册账号,提交报名,教师端能否看到待审核记录,审核通过后学生端状态是否更新
- 竞赛报名人数达到上限后,再点报名是否被正确拒绝
- 竞赛从“报名中”改为“评审中”后,学生端是否还能报名,是不是报了也能报成功
- 一个学生被学院管理禁用后,使用旧的JWT能否继续访问接口
自动化测试不一定覆盖全部,但接口层至少要写几个核心的集成测试。比如使用Spring Boot Test + MockMvc模拟登录、报名、审核完整的链路。
6.2 并发场景的简单验证
报名高峰期可能就那几秒钟,几百个学生同时提交报名,数据库能不能扛住?虽然量不大,但还是要测一下。用JMeter或者Postman的Runner功能模拟100个并发报名请求,观察是否有超卖、是否有死锁。如果用的是上面的原子update方案,基本不会出问题。但如果写成先select判断再update,并发下很容易出问题。这个坑我在真实项目中踩过一次,最后全表扫了一遍把重复数据清理掉,非常狼狈。
6.3 兼容性测试
前端别只盯着Chrome看。高校机房里的浏览器有好几种,老版本Edge、360安全浏览器兼容模式之外,还有Linux实验室里可能没有图形界面,学生用的是手机浏览器。至少保证Element Plus部分控件在手机端可用,table在小屏幕上可以横向滚动。管理员操作主要在电脑上,但学生报名很可能是手机操作的,可以考虑给报名页面单独做一个简化版本,或者直接用组件库的栅格系统做响应式适配。
7. 常见问题与排查技巧实录
7.1 前端接口跨域报错
初学者最常见的报错就是No 'Access-Control-Allow-Origin' header is present on the requested resource。原因大概率是后端没有配置CORS,或者前端开发地址和后端地址不同。解决办法有三:后端加@CrossOrigin注解、全局CorsFilter配置、前端devServer proxy。如果要上线,前后端用Nginx同域部署,根本不存在跨域问题,所以开发阶段用proxy最省心。
7.2 页面刷新404
这个问题我在前面提到过,history模式下Nginx没有配置try_files,刷新后或者从外部访问深层链接时,Nginx找不到对应的实体文件,就会返回404。排查时先看浏览器地址,如果URL是/competition/list,说明确实是前端路由问题。直接在nginx.conf加try_files即可。
7.3 JWT过期后页面还在
用户登录后打开系统,超过2小时再点击操作,前端拿到的token已经失效,后端返回401。前端路由守卫和响应拦截器要做联动,收到401后清理本地存储和当前用户信息,然后跳转到登录页。有个容易忽略的细节:多个请求同时返回401时,不能每个都跳转一次,要加一个标志位,只跳转一次,否则用户会被反复弹到登录页。
7.4 后台统计数字对不上
竞赛获奖统计和报名统计经常对不上,原因一般是:报名记录被逻辑删除了,但统计SQL没过滤deleted=0;或者获奖表中的学生来自多届,但统计维度没有加学年筛选。我的经验是:所有的统计SQL,底层必须从一个统一的“事实表”出发,不要各写各的。比如“学生参赛人数”以报名表status=1为准,“获奖人数”以获奖表数据为准,两个指标的定义在文档里写清楚,不要让后人猜测。
7.5 日期时间差8小时问题
很多初学者把时间直接存成VARCHAR,或者后端返回LocalDateTime,前端解析时没有指定时区,结果时间显示比实际少了8小时。规范做法是:数据库统一使用datetime类型,时间字段不要存字符串,Jackson序列化时指定时区:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8如果前端用了dayjs,也建议统一格式化时间后再展示。我曾经因为在微信小程序里没做时区处理,导致学生看到的报名截止时间比实际早8小时,直接错过了校内赛,被辅导员追着问了三天。
8. 二次开发与扩展建议
8.1 引入消息通知与邮件服务
目前站内通知已经够用,但部分比赛需要给指导教师发送邮件提醒。可以引入Spring Mail,在报名审核通过后自动发送一封邮件到老师邮箱。配置很简单,就是设置SMTP地址、账号、密码。需要注意:高校的邮件服务器对发件频率有限制,如果通知量大,建议做异步发送,用@Async标注发送邮件的方法,同时配置一个线程池。
8.2 集成教务系统数据
如果要做真正的学生身份认证,就需要和学校统一身份认证平台对接,通常用CAS或OAuth2.0。这套系统的登录方式就可以从账号密码登录,调整为跳转到学校认证中心,回调后自动注册或绑定本地用户。实现上不建议自己造轮子,直接用CAS客户端或OAuth2客户端。在校内环境下,最省事的还是先保持本地账号,等需求明确后再单独做对接方案。
8.3 证书生成与附件管理
获奖证书生成是新增需求,可以结合POI-TL(Word模板渲染)生成套打证书,也可以生成PDF。这套思路和成绩导入类似,后端先生成临时文件,前端下载。文件存储不要放数据库,建议用本地磁盘目录,按竞赛ID分目录存放,或者接入MinIO做对象存储。MinIO部署简单,提供S3协议,替换掉本地文件存储的时间成本不高,值得后续试试。
8.4 数据导出Excel防中文乱码
导出报名名单和获奖名单很常见,使用EasyExcel时需要在导出前设置响应头:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("报名名单.xlsx", "UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName);如果不URLEncoder编码文件名,浏览器下载时中文会乱码。另外,单元格内容不要用拼接的方式写日期,用Excel里的日期格式,否则后续用Excel筛选时间时全是文本。
9. 总结一下这套系统的实施要点
做到这里,整套高校竞赛管理系统的骨架已经清晰了。从需求上看,它覆盖了从角色权限到竞赛全生命周期的核心链路;从技术上,SpringBoot负责后端业务逻辑,Vue负责交互体验,MyBatis包揽复杂SQL,MySQL承载数据,四者各司其职。这个项目最大的价值在于,它不是纯粹的技术框架学习,而是一个能真正落地使用的业务系统。
最后分享一点我个人的实操体会:做这类项目,别一上来就写代码,先花两天把角色、状态、流程想清楚,数据库表结构哪怕多改几次都没关系,但接口设计要尽量稳定。接口命名规范、统一返回结构、统一异常处理,这些“不性感”的基础工作,反而是系统后期维护时最省心的地方。我见过很多代码漂亮花哨但接口东一个西一个的项目,上线后维护成本高得让人想离职。
如果你正在用这个题目做毕业设计或者项目经历,我的建议是:先把核心链路跑通,把权限和状态流转做扎实,再考虑加Excel导入导出、消息通知这些锦上添花的功能。面试时能把这个系统从表设计讲到并发控制,从JWT讲到Nginx部署,已经足够证明你有做全栈项目的综合能力。动手改起来吧,踩坑的过程才是最值钱的经验。