
1. 项目概述1.1 这个系统到底解决什么问题还在为学科竞赛报名信息散落在各个班级群、Excel表格传来传去而头疼吗还在为评审打分需要手工统计、一张张纸质评分表翻到手软而苦恼吗如果你带过竞赛组织工作或者正在做Java方向的课程设计、毕业设计那你应该对这套springboot学科竞赛管理系统的价值非常敏感。这套系统本质上是一个典型的校园业务管理平台核心用户分三类管理员教务处/竞赛组织者、参赛学生、评审专家评委。它把赛事创建、报名审核、作品提交、评委打分、成绩公示、证书发放这一条完整的竞赛生命周期搬到了线上替代了过去依靠Excel汇总、邮件报送、纸质签字的老旧流程。从我个人的角度看这类项目之所以在毕设和课设中经久不衰是因为它覆盖了Web开发中最核心的能力矩阵权限控制、CRUD操作、文件上传、状态流转、数据统计而且业务逻辑清晰、边界明确非常适合用来展示一个开发者对springboot框架的掌握程度。源码86355这套命名暗示的是项目归档编号也就是一份完整的可运行工程后面我会以这套代码的典型实现路径为基础把整个系统的设计思路和落地细节完整拆给你看。1.2 适合谁来读这篇文章如果你是正在做毕设的本科生这篇文章能帮你快速搞懂学科竞赛管理系统的数据表怎么设计、接口怎么划分、权限怎么控制如果你是准备面试的Java开发岗求职者可以从中提取真实项目经验描述话术知道在项目介绍环节怎么讲出亮点如果你是高校教务或竞赛组织人员也能理解这类系统在实际落地时有哪些坑、哪些功能才是真正刚需。我会尽量用实战的视角来写——不只是给你罗列功能模块而是告诉你每个设计决策的背后原因比如“为什么这里用枚举状态而不是布尔值”“为什么报名表要单独拆一张表而不是在用户表里加字段”。这些细节才是区分一个“能跑”的项目和一个“能答辩、能面试拿出手”的项目的关键。2. 业务模型与功能架构拆解2.1 用户角色与权限边界设计整个系统的逻辑起点不是代码而是角色。学科竞赛的参与链条天然就是三条线管理者发布规则、学生报名参与、评委评审作品。如果一开始不把角色理清楚后面所有的接口设计和页面开发都会陷入“谁都能点所有按钮”的灾难中。这套系统的推荐做法是用户表sys_user中增加role字段取值用ADMIN/STUDENT/JUDGE三个枚举表示。这里有个容易被新手忽视的点——不要直接在前端用v-if判断角色来隐藏按钮真正的权限校验必须在后端做。因为接口是可以被直接调用的前端隐藏菜单只能起到“眼不见为净”的视觉效果并不能阻止一个学生用户通过Postman直接调用管理员的删除接口。后端的权限控制推荐使用Spring Boot 拦截器 自定义注解的方案。在需要管理员权限的接口上标注RequireRole(ADMIN)拦截器统一校验当前登录用户的角色。相比引入Spring Security这类重量级框架自定义方案对毕设和中小型系统来说更轻量而且碰到面试官问“权限怎么设计的”你能把底层逻辑讲清楚反而比背Spring Security的过滤器链更有说服力。2.2 赛事全生命周期状态机竞赛不是一个瞬时动作而是一条有明确阶段的时间线。如果你只用status字段存一个字符串“报名中”那后面每次状态变更都需要写一堆if-else一旦某个角落忘了处理就会出现“还在评审阶段学生居然还能修改作品”这种逻辑漏洞。我建议把竞赛状态设计为显式的状态机整个生命周期包含以下状态状态含义可执行操作后续可流转状态DRAFT草稿编辑、发布PUBLISHEDREGISTERING报名中学生报名、管理员审核REVIEWING / CLOSEDREVIEWING评审中评委打分、管理员查看进度FINISHEDFINISHED已结束成绩公示、证书生成-CLOSED已关闭仅可查看-在代码里我习惯用一个CompetitionStateMachine类统一封装状态流转逻辑提供transition(currentState, targetState, role)方法来判断某次状态变更是否合法。这样做的好处是全局只有一处状态规则不会出现某个状态下漏判的情况。实测下来这种设计在旧的代码上加上去成本也不高大部分就是一个Map 一个校验方法的事情。从使用角度讲状态机的存在能避免很多实际问题比如学生误在评审阶段修改了作品、管理员忘记关闭报名导致截止后还有人提交、评委在成绩未公布时就能看到自己打的分。这些都是竞赛管理系统最让人焦虑的细节状态机就是用来根治这类问题的。2.3 模块划分与页面结构按照后端接口的维度系统可以拆成六大模块用户管理登录注册、个人信息维护、密码加密存储赛事管理赛事CRUD、指定评委、赛事公告发布报名管理学生报名、撤销报名、管理员审核作品管理作品上传文档/压缩包、作品列表、作品下载评审管理评委打分、评分汇总、成绩排名证书管理获奖名单生成、证书模板下载相对应的前端页面基本是一对一的映射关系。这里有一个设计取舍值得展开说——建议后端接口严格按RESTful风格设计但前端页面不要弹窗套弹窗。很多同学在写这类管理系统时喜欢把“创建赛事”放在一个模态框里赛事创建完又弹“添加评委”评委添加完又弹“发布公告”。这种交互在毕设答辩演示时特别容易翻车因为操作路径太长评委看不到整体感。更好的做法是赛事列表页展示所有赛事卡片点击某个赛事进入赛事详情页详情页用Tab页签区分“报名管理”“作品列表”“评审进度”“成绩发布”四个子页面。这样天然就形成了“父进子”的页面层级功能也不会挤在一起。3. 核心技术与数据模型设计3.1 技术选型的理由这套系统最稳妥的技术栈组合是Spring Boot 2.7.x MyBatis Plus MySQL 8.0 Vue 2 Element UI Apache POI JWT逐个说理由。Spring Boot 2.7.x 是3.0之前最成熟的版本网上资料最多遇到问题搜得到3.x虽然已经推出但部分老教程不兼容对毕设和快速交付反而有坑。MyBatis Plus相对于原生MyBatis最大的价值是内置了BaseMapper单表CRUD几乎不用写SQL能把开发速度提升一大截。MySQL 8.0 是当前主流版本注意jdbc驱动要对应com.mysql.cj.jdbc.Driver这在代码里属于老生常谈但也确实是最常见的启动报错点。前端为什么推荐Vue 2 Element UI而不是Vue 3 Element Plus因为Vue 2生态成熟、组件丰富、报错信息好查对新手更友好。如果你打算挑战Vue 3当然没问题但请做好“文档查半天、组件不兼容”的心理准备。还有一个实用技巧是开发阶段用Vite/Webpack的代理解决跨域部署阶段把前端打包后放进Spring Boot的src/main/resources/static目录下这样最终交付的就是一个单进程的jar包拷到哪都能跑答辩时再也不用折腾Nginx和跨域问题。3.2 数据表设计与核心字段解释数据模型是整个系统最终要落地的根。表如果设计错了后面写多少层Service都补不回来。下面这张表结构是我多次实践后觉得比较合理的方案。sys_user用户表字段名类型说明idbigint主键自增usernamevarchar用户名唯一索引passwordvarcharBCrypt加密后的密码real_namevarchar真实姓名rolevarcharADMIN / STUDENT / JUDGEstudent_novarchar学号学生角色必填phonevarchar手机号emailvarchar邮箱avatarvarchar头像URLenabledtinyint是否禁用账号created_atdatetime创建时间密码存储这块要重点提一句不要用MD5不要用明文。MD5加盐之后依然不够安全推荐使用BCryptPasswordEncoder。Spring Security的crypto模块可以独立引用这个加密器不需要引入完整的Security依赖这样既保证了密码安全又不用处理Security的过滤器链简直是毕设的最佳折中选择。competition赛事表字段名类型说明idbigint主键titlevarchar赛事名称descriptiontext赛事描述/参赛要求cover_imagevarchar封面图URLregistration_startdatetime报名开始时间registration_enddatetime报名截止时间review_startdatetime评审开始时间review_enddatetime评审截止时间max_team_sizeint每组最大人数statusvarchar状态见上文状态机creator_idbigint创建人ID管理员created_atdatetime创建时间competition_registration报名表字段名类型说明idbigint主键competition_idbigint关联赛事IDteam_namevarchar团队名称leader_idbigint队长ID关联sys_usermember_idstext队员ID列表JSON字符串contact_phonevarchar联系电话statusvarcharPENDING / APPROVED / REJECTEDreject_reasonvarchar审核拒绝原因created_atdatetime报名时间这里有个经验分享member_ids用文本字段存JSON而不是新建一张独立的团队关联表是为了降低理解成本和代码量符合毕设/课设场景。如果你做的是企业级项目建议拆表因为第三范式更严格但作为教学项目这个设计能让你省去大量join的麻烦。competition_work作品表字段名类型说明idbigint主键registration_idbigint关联报名记录IDtitlevarchar作品名称descriptiontext作品简介file_urlvarchar作品文件路径file_namevarchar原始文件名下载时用submit_timedatetime提交时间versionint版本号支持多次提交作品表加version字段是我特别想强调的一个点。很多数竞赛报名了的人中途改作品是很常见的事如果作品表只有一行记录学生改了就得覆盖原文件万一改错了想恢复就找不回来了。版本号的设计逻辑是一次提交算一个版本后端不做真正的覆盖而是存多一条历史记录version递增。管理员和评委看的是最高版本学生本人可以回看历史版本。这个功能在答辩现场很容易成为加分点因为体现了“你考虑过实际业务场景”。competition_score打分表字段名类型说明idbigint主键work_idbigint关联作品IDjudge_idbigint评委IDinnovation_scoredecimal创新性得分0-20completeness_scoredecimal完整性得分0-30presentation_scoredecimal表达展示得分0-30practicality_scoredecimal实用价值得分0-20total_scoredecimal总分100分制commentvarchar评语created_atdatetime打分时间分项打分比直接填一个总分要合理得多。一方面评分维度更明确评委操作起来有依据另一方面管理员在成绩汇总时可以按权重重新折算——比如“创新性占40%、其他占60%”——这就让系统具备了灵活的权重调整能力而不只是把总分存死。3.3 数据库初始化脚本的落地经验建表SQL不要手写一条条执行建议直接在项目中放置一个db/init.sql文件把建库、建表、插入基础管理员账号的语句都写好交付时别人一键执行就完成初始化。这条SQL文件同时也是你答辩文档的重要组成部分评审老师一定会看。初始化数据至少要包含一个管理员账号admin / admin123密码存BCrypt密文、一个测试评委账号、两个测试学生账号、两三条演示用的竞赛数据、几条报名记录、几条评分数据。这样系统一启动所有页面都有数据展示演示效果远好于一个空荡荡的列表页。4. 核心功能实现与实操步骤4.1 认证与授权模块的实现路径登录接口的完整逻辑我用伪代码拆解一下这是整个系统的安全门槛必须稳。public Result login(String username, String password) { // 1. 查询用户 User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, username)); if (user null) { return Result.error(用户不存在); } // 2. 密码校验BCrypt if (!bcryptPasswordEncoder.matches(password, user.getPassword())) { return Result.error(密码错误); } // 3. 检查账号是否被禁用 if (user.getEnabled() 0) { return Result.error(账号已被禁用请联系管理员); } // 4. 生成JWT令牌 String token jwtUtil.generateToken(user.getId(), user.getRole()); // 5. 组装登录结果 return Result.success(new LoginResponse(token, user.getRole(), user.getRealName())); }JWT的密钥建议放在application.yml配置里通过Value注入而不是硬编码在类中。token有效期可以根据实际场景设长一点比如24小时毕竟校内系统对会话时效的要求没那么严格太短反而导致用户频繁重新登录。另一个容易被忽视的细节是密码错误提示要统一。从用户角度我只需要知道“用户名或密码错误”不需要知道到底是用户名错了还是密码错了防止别人通过提示信息反推出有效用户名。拦截器注册部分重写WebMvcConfigurer.addInterceptors排除登录和注册接口其他接口全部走JWT校验。校验逻辑就三步从请求头取Authorization字段 - 用JWT密钥解析token - 解析成功就把用户信息放到请求上下文中。4.2 赛事管理与报名的完整交互流程赛事创建之后的用户视角我用一个顺序来展示管理员创建赛事填写标题、描述、报名起止时间、评审起止时间、队伍人数限制赛事状态为REGISTERING学生端赛事列表能看到该赛事状态筛选REGISTERING学生进入赛事详情页提交报名填团队名称、选队员从学生列表里搜索、填联系方式管理员在“报名管理”Tab中看到PENDING状态记录点击通过或否决否决必填原因审核通过后学生端出现“作品上传”入口学生上传作品支持压缩包/PDF/Word后端保存文件并生成file_url上传时间截止后管理员把赛事状态改成REVIEWING评委登录后看到分配给自己评审的作品列表分配规则管理员可在赛事详情中手工将作品分给评委评委逐份打分、填写评语提交后不可修改如需修改管理员需开放“重新评审”开关评审时间截止管理员把赛事状态改为FINISHED系统根据total_score排序生成排名与获奖等级这10步理清楚之后整个系统的编码方向就非常明确了。很多同学上手就写代码结果功能写到一半才发现流程缺陷比如“学生交作品时赛事已经结束了”这些问题都是因为没有把流程先以文字形式固定下来。建议你写代码前先用Word或表格把类似上面的流程列出来团队协作时更是如此这能省掉至少一半的返工时间。4.3 文件上传与成绩导出的实现细节文件上传是所有系统中“看起来简单、做起来坑最多”的功能。后端我推荐使用本地磁盘存储路径放在配置文件的file.upload-dir项里。上传接口核心逻辑如下public Result uploadWork(MultipartFile file, Long registrationId) { // 1. 校验文件是否为空、是否超过大小限制 if (file.isEmpty() || file.getSize() MAX_SIZE) { return Result.error(文件为空或超过大小限制); } // 2. 生成唯一文件名防止中文乱码与重名覆盖 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) ext; // 3. 按日期分目录存储避免单目录文件过多 String datePath LocalDate.now().toString(); // 2026-01-10 File dir new File(uploadDir / datePath); if (!dir.exists()) { dir.mkdirs(); } // 4. 保存文件 File dest new File(dir, newFilename); file.transferTo(dest); // 5. 将文件信息插入作品表 return Result.success(fileUrl); }有几个细节分别说一下。第一文件夹按日期分层我见过有人把所有文件堆到一个目录里的结果几百个文件后管理混乱、备份也困难。第二文件名用UUID并保留原扩展名是为了兼容中文名文件下载时Content-Disposition的编码问题——文件名直接用中文字符串存储下载时经常变成一堆乱码或直接报错。第三文件大小限制要在Spring Boot的配置里同时设置分别是spring.servlet.multipart.max-file-size和max-request-size默认只有1MB如果你不调大上传稍大的PPT或者PDF就会被拦。成绩导出用Apache POI生成Excel的套路比较成熟核心就是用XSSFWorkbook创建工作簿、Sheet创建表格、Row创建行、Cell创建单元格最后用FileOutputStream写出。导出时有一个细节需要特殊处理评委打分表是分项的创新、完整、表达、实用导出的汇总Excel里应该把分项列出来最后再算一个总分列这样管理员拿到后既可以直接用又方便二次核验。4.4 团队报名中成员选择的实现技巧成员选择这个功能看似简单实际写的时候很多人会卡住。核心难度在于前端页面怎么做成员选择的交互以及成员数据以什么形式传给后端。我的建议是不要直接在表单里放一个多选下拉框然后一股脑全选——那样在几十个学生的情况下体验很差。更实用的方案是页面上提供一个“添加成员”按钮点击弹出学生搜索对话框按姓名或学号模糊搜索找到后添加进入成员列表以卡片/标签形式展示。成员列表下方用隐藏字段存储JSON数组提交时JSON.stringify(memberList)传回后端后端用parseArray解析后存入member_ids文本字段。搜索接口用 MyBatis Plus 的LambdaQueryWrapper是最顺手的方式ListUser students userMapper.selectList(new LambdaQueryWrapperUser() .eq(User::getRole, STUDENT) .like(StringUtils.isNotBlank(keyword), User::getRealName, keyword) .or() .like(StringUtils.isNotBlank(keyword), User::getStudentNo, keyword) .last(limit 20));这个接口在审核界面也同样适用管理员可以根据姓名快速找到某个报名的学生并查看他的参赛记录。一个接口两种用途这个设计在前端看来就是“搜索即复用”。4.5 评委端评审页面与打分联动评委登录后页面展示分配给自己的待评作品列表。每份作品点进去是一个评审详情页左边显示作品文件预览/下载按钮和作品描述右边是评分表单。评分表单每个分项用el-input-number组件设置min0、max20按分项满分配置、step1并且配一个实时计算总分的显示。前端实现总分实时计算的逻辑很简单就是监听四个分项值的变化求和然后填到总分字段中。这里有一个实用技巧总分只要展示就可以了不要用前端的手动输入框给评委手动改总分的机会。总分一律由后端重新计算防止前端被篡改数据也避免评委填了分项和总分不一致的脏数据。评审提交之后这条评分记录的状态就变为已提交。评委端页面区分“待评审”“已完成”两个Tab已完成列表只读不可再改。给评委或管理员预留的“重新开放评审”的功能实现逻辑其实就是把该条记录加一个review_status字段从“已完成”改回“待评审”这样评委就能重新提交了。这个功能在真实场景中非常实用比如某个评委填错了分数就需要管理员介入。5. 前后端联调、接口文档与项目部署5.1 前端开发时的跨域与接口封装跨域问题是前后端分离开发时最容易卡住的点。开发模式下前端跑在8080端口后端跑在8081端口前端发axios请求到后端必然产生跨域。解决方案有两个方案一是后端加跨域配置。在Spring Boot中增加一个CorsConfig配置类重写CorsFilter允许本地开发地址的请求即可。方案二是前端配置代理。以Vue项目为例在vue.config.js中配置 devServerdevServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } }我个人的偏好是开发期间两个方案同时用后端开启跨域前端也配置代理双保险。部署后因为前端已经打包进静态目录同源访问跨域问题自动消失。接口封装的统一思路是创建一个request.js文件统一配置baseURL、超时时间、请求拦截器attach token和响应拦截器统一解析code、处理401。这样做的好处是所有的请求错误处理都收敛到一处不会出现每个页面都写一遍错误提示的冗余代码。5.2 快速理清API接口设计后端接口按模块划分可以整理成下面这张表不管你是自己开发还是前后端联调先把这个表列清楚再动手写代码效率会翻倍。功能模块接口路径方法说明认证/api/auth/loginPOST登录认证/api/auth/registerPOST学生注册认证/api/auth/infoGET获取当前用户信息赛事管理/api/competition/listGET分页查询赛事赛事管理/api/competition/detail/{id}GET赛事详情赛事管理/api/competition/createPOST创建赛事管理员赛事管理/api/competition/updatePUT更新赛事管理员赛事管理/api/competition/delete/{id}DELETE删除赛事管理员赛事管理/api/competition/changeStatusPOST变更赛事状态报名管理/api/registration/submitPOST学生提交报名报名管理/api/registration/cancel/{id}POST撤销报名报名管理/api/registration/reviewPOST审核报名管理员报名管理/api/registration/listByCompetitionGET赛事下的报名列表作品管理/api/work/uploadPOST上传作品作品管理/api/work/listByRegistrationGET查看某报名下的作品列表作品管理/api/work/download/{id}GET下载作品文件评审管理/api/score/submitPOST提交评分评审管理/api/score/listByJudgeGET评委查看自己的评审列表评审管理/api/score/listByCompetitionGET赛事的所有评分记录管理员统计导出/api/competition/exportScore/{id}GET导出Excel成绩单后端开发时不要急着写代码先把Controller层的接口路径全部定义好返回统一的Result对象然后再逐层向下填充。这看起来慢实际反而快因为服务和数据层可以并行开发。5.3 打包部署与常见环境问题打包部署是交付前的最后一道工序也是最容易出现环境问题的地方。核心打包命令# 后端打包 mvn clean package -DskipTests # 前端构建 npm run build构建完成后把前端dist目录下的所有文件拷贝到后端的src/main/resources/static目录下再重新执行一次mvn clean package -DskipTests此时得到的一个可执行jar包就是完整系统。启动命令java -jar competition-system.jar --server.port8080部署阶段三个最高频的坑提前帮你排掉第一MySQL时区问题。连接串里要加上serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8否则会出现“Server returns invalid timezone”和中文乱码。第二端口被占用。启动时报Port already in use排查命令netstat -ano | findstr 8080找到PID之后结束进程或者直接换端口启动。这个问题80%以上的人都会遇到原因不外乎是上次运行的程序没有真正停止。第三数据库版本不一致。如果你的MySQL是5.7驱动版本选了mysql-connector-java 8.x会出现连接失败的报错。解决办法有两个要么换5.7对应的驱动版本要么把数据库升级到8.0。就我的经验来说直接统一用8.0最省心因为8.x驱动向下兼容场景很有限别再折腾兼容性了。5.4 答辩演示的准备建议如果你的目标是拿这套系统去做毕业设计答辩有几个演示细节一定要提前准备演示时不要从登录页开始因为评委见过太多人输入账号密码再点登录的流程了。更好的演示路径是首页仪表盘展示统计图表→赛事列表进入详情→报名审核界面→查看一个学生作品→评委打分界面→成绩导出Excel。这条路径每一个画面展示的都是系统的核心能力前后衔接自然不会冷场。还要提前准备三个“如果”场景如果评委问“系统安全性怎么保证”你的回答聚焦在BCrypt加密密码、JWT拦截器、后端角色校验三点上如果问“数据怎么统计的”回答聚焦在Excel导出和前端图表可视化如果问“最复杂的业务逻辑是哪个”回答多角色状态流转下的报名-评审-发布闭环把状态机那套表达出来这就是项目的深度所在。6. 常见问题与排查技巧实录6.1 登录页一直转圈/请求404这个问题高频出现通常不是后端逻辑问题而是前端代理没配对。检查步骤按顺序来打开浏览器F12查看请求地址是否真的发到了后端如果看清请求发的是/api/login而代理规则匹配不到就是pathRewrite的路径写错了。还有可能是后端接口路径是/api/auth/login前端却写成了/api/auth/login/多一个斜杠404概率极大。排查思路其实很简单先看网络请求本身通不通再看路径是否精确匹配最后看是否被拦截器拦截了。6.2 图片上传成功但访问不到如果你用本地磁盘存储文件映射路径是/upload/**需要在WebMvcConfigurer中配置虚拟路径映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); }不配置这个映射数据库存的file_url是/upload/2026/xxx.png浏览器却访问不到。这点在本地开发时容易被忽略因为直接用file:///E:/xxx.png能打开但浏览器通过HTTP访问不行。记住文件存在磁盘HTTP访问需要虚拟目录映射这是两件事。6.3 分页查询数据对不上MyBatis Plus 的分页查询需要单独配置分页插件否则selectPage方法会返回全量数据分页失效。正确配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }另外一个容易忽略的细节是分页查询时关联查询比如查询赛事列表同时要算出已报名人数和审核通过人数不要依赖多表join更简单的做法是先查出当前页的赛事ID列表再用IN查询批量统计最后在内存中组装。这在大数据量下性能表现也足够好且代码逻辑清晰。6.4 评委重复打分导致数据错乱系统允许评委对作品打分但不能让评委对同一份作品打两次分。如果competition_score表没有加唯一约束重复评分就会导致一个评委对同一作品出现两行记录成绩汇总就会出现“取最高分还是取平均分还是取最新一条”的争议。解决方案分两层数据库层面给work_id和judge_id加联合唯一索引代码层面提交评分前先selectOne查一下是否已存在记录存在则返回“已评过分”。数据库约束兜底 代码该判就判这个双保险值得做。6.5 Excel导出中文列名乱码Apache POI导出时中文列名乱码或下载后Excel提示文件格式错误大部分情况是响应头设置不对。导出时正确设置响应头是关键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);如果不设置Content-Disposition浏览器会把内容当作纯文本显示出来。文件名用URLEncoder.encode编码也是惯用做法兼容各种浏览器下载时的中文文件名。7. 项目扩展方向与实用经验7.1 给这个系统再加几个亮点如果这套系统不仅仅是用来交差而是想做得更有深度下面这几个方向值得尝试接入ECharts做可视化分析。当前系统的数据量虽然不大但管理员首页如果有一张“各学院参赛人数排行”的柱状图、“历届赛事报名趋势”的折线图整个系统的视觉档次会提升一大截。ECharts的难点不在配置而在于后端怎么把数据聚合好返回给前端这里你至少要会写GROUP BY的SQL。引入消息通知机制。学生报名审核通过、成绩已经公布、竞赛即将截止这些关键节点都应该触发站内信通知。消息表设计很简单notification(id, user_id, title, content, is_read, created_at)。这个功能最大的价值是让系统“活”起来而不是用户隔三差五刷新页面才能看到变化。考虑评审盲评机制。在某些竞赛场景下为了公平性评委打分时不应该看到学生的姓名和学校信息也就是“盲审”。实现起来其实就是作品表和展示字段做一层脱敏——评审界面不展示报名者的姓名。这个功能展示的是你对竞赛业务公平性的理解在答辩时说到这一点会非常加分。7.2 我在实际开发中的几点体会说几个代码之外的真实体会。第一个是关于“数据库字段设计”的。很多同学在开发初期喜欢把时间字段存成字符串因为前端传来的就是字符串但这样会让后续所有时间比较、区间筛选变得极其痛苦。我就吃过这个亏——有一次要查“报名截止时间在昨天之前的学生”用字符串比较时格式稍微不对就查不到。从第一天开始就用datetime类型后端用LocalDateTime接收这是能让你省心到最后一刻的选择。第二个是关于“不要过度设计”。有同学刚学了Redis就想着把赛事列表、用户信息全部缓存起来刚学了消息队列就想把所有操作都发个消息。对于学科竞赛管理系统这个体量数据库直接查询是完全够用的加缓存反而引入缓存与数据库的一致性问题让你排查半天。技术选型要匹配系统规模这是我在参与过多个实际项目后越来越有感触的。第三个是关于“演示数据的重要性”。开发完系统先别急着交花半小时往库里填一批看起来“真实”的数据——学生的学号命名为2023开头、团队名称用“飞鸟创新队”而不是“test01”赛事的封面图选一张正式的横幅图。这些细节决定演示的时候是“一个粗糙的教学案例”还是“一个能直接落地的业务系统”在答辩现场的体感差距非常明显。7.3 再分享一个微信扫码登录的小彩蛋如果为了给系统增加一点亮点你可以试着给学生端加一个二维码登录。实现原理并不复杂前端生成一个二维码二维码内容是一个随机的loginToken后端把loginToken与用户ID关联存储比如放到Redis里学生用手机扫码后跳转到后端的一个H5登录页面输入账号密码登录成功后后端更新loginToken与用户的绑定关系前端页面轮询这个loginToken的状态发现已绑定则自动跳转登录态。这个功能在面试或答辩中是个非常亮眼的增量因为它涉及“扫码轮询”这个常见于企业级应用的场景遵循的技术也很常考——唯一身份令牌、短时失效、轮询接口。给学科竞赛管理系统加上它整个项目的交互体验和话题度都会完全不一样。这套系统说到底不是一个多高深的项目但它是一个非常标准的、完整的业务闭环。把这套流程吃透了以后换一个业务场景比如毕业论文选题系统、实验室设备借用系统你都能快速复制这套框架——因为底层的用户体系、状态流转、文件管理、数据导出这些能力是通用的。写代码不难难的是在动手前把业务想透把边界划清这个道理在这类管理系统上体现得淋漓尽致。