
在省级职业技能竞赛中拿到全省第三名是值得高兴的成绩但往往也最让人不甘心。距离二等奖可能只有 0.3 分而复盘时才发现失去的分数并不是因为某个算法不会写而是暴露在工程流程、稳定性、演示细节和团队协作上。本文以“校园赛事管理可视化平台”比赛项目为背景完整拆解该项目的技术选型、数据库设计、后端接口、前端可视化、部署上线与答辩演示全流程同时把导致“全省第三遗憾”的高频问题单独拿出来复盘。无论你是在准备下一届技能竞赛还是想把课程设计升级成能拿得出手的比赛项目这篇文章都值得收藏。1. 比赛项目复盘全省第三的遗憾是怎么产生的1.1 比赛项目不是课程设计很多参赛队伍一开始就把比赛项目当成“大号课程设计”来做后端写几个接口前端套一个后台模板数据库建几张表能跑就行。这样的项目在平时练习中看不出问题一旦到了比赛现场问题会集中爆发。课程设计关注“功能有没有”比赛项目关注“系统好不好用、稳不稳定、能不能讲清楚”。同样是“成绩管理”课程设计只需要增删改查比赛项目却要回答并发导入大量成绩时数据库会不会卡死评委随机输入一个查询条件接口响应是否迅速演示过程中断电、断网、浏览器崩溃团队能否快速恢复项目代码是否规范答辩时能否清晰说明表结构和核心逻辑这些问题的背后是工程化能力。全省第三这个名次恰恰说明基础功能完成度较高但在工程化维度上输给了前面的队伍。复盘的意义就是把这些隐藏的失分点找出来。1.2 比赛项目功能规划以“校园赛事管理可视化平台”为例一个典型的比赛项目通常包含以下功能模块功能说明技术重点登录认证管理员、教师、学生角色登录JWT、RBAC 权限模型基本信息管理学生、赛事、班级信息维护数据库设计、分页查询成绩管理成绩导入、修改、审核Excel 导入、批量更新数据可视化参赛人数、获奖率、趋势分析ECharts、聚合查询报告导出生成成绩分析报告POI 导出、文件下载系统管理用户、菜单、日志管理日志切面、操作审计在比赛现场功能不用贪多但每个功能都要经得住深挖。评委不会因为页面多就加分反而会因为“功能看似很多实际无法演示完整流程”而扣分。1.3 为什么“全省第三”会有遗憾从成绩单上看决赛总分往往由几个维度组成功能实现、性能表现、创新性、文档质量、答辩表现。大多数队伍在“功能实现”上都能拿到 80% 以上的分数真正的分差出现在以下几处演示环节突然报错扣稳定性分数据库没有测试数据现场可视化大屏图表空白代码结构混乱答辩时被问到某个 Controller 的作用答不上来安全校验缺失评委现场提交一个畸形请求系统直接 500部署文档不全现场环境与开发环境不一致启动失败。这些失分点和技术难度无关却决定了名次。这也是为什么很多团队拿到“全省第三”后复盘时会觉得遗憾——不是不会做而是没有把工程细节做到位。2. 环境准备与项目结构2.1 开发环境版本说明比赛项目不需要追求最新的技术版本稳定、熟悉、可复现才是核心。下面是一份常见的技术栈清单版本需要根据团队实际环境调整重点演示配置思路。技术项推荐选择说明操作系统Windows 10/11、Linux开发环境与比赛环境尽量一致JDKJDK 1.8 或 JDK 17根据 Spring Boot 版本选择构建工具Maven 3.6统一依赖管理后端框架Spring Boot MyBatis-Plus快速开发适合比赛项目数据库MySQL 5.7 / 8.0注意字符集设为 utf8mb4缓存Redis用于会话缓存、热点数据前端框架Vue 3 Element Plus组件化、易维护图表库ECharts 5可视化大屏核心部署方式Nginx jar 包前后端分离部署比赛之前团队应该把“一键启动”作为目标。后端用 Maven 打包前端用 npm 构建最终把部署步骤整理成文档。一个能在 5 分钟内启动成功的项目比一个功能丰富但无法快速部署的项目更有优势。2.2 项目结构前后端分离的项目建议使用下面的目录结构campus-competition/ ├── backend/ │ ├── src/main/java/com/example/competition/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── mapper/ │ │ ├── entity/ │ │ ├── config/ │ │ └── common/ │ ├── src/main/resources/ │ │ ├── application.yml │ │ └── mapper/ │ └── pom.xml ├── frontend/ │ ├── src/ │ │ ├── api/ │ │ ├── views/ │ │ ├── components/ │ │ └── router/ │ ├── package.json │ └── vite.config.js ├── sql/ │ └── init.sql └── README.md这里需要特别强调README.md的重要性。比赛答辩前评委可能会直接打开项目仓库查看文档。一个结构清晰、说明完整的 README能大幅提升评委的第一印象。README 至少应该包括项目简介、技术栈、启动步骤、默认账号、目录说明和演示地址。3. 数据库设计与核心接口3.1 数据库表设计数据库设计决定了业务逻辑的复杂度。设计得不好后期写接口会非常痛苦。以成绩分析为核心建议至少包含以下表sys_user用户表student学生表competition赛事表score成绩表下面是简化版的建表 SQLCREATE DATABASE IF NOT EXISTS campus_competition DEFAULT CHARSET utf8mb4; USE campus_competition; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) NOT NULL, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 用户表; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), major VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE competition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, level VARCHAR(20), start_date DATE, end_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 赛事表; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, competition_id BIGINT NOT NULL, score DECIMAL(5,2), rank_no INT, is_award TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_competition_id (competition_id), KEY idx_student_id (student_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_competition FOREIGN KEY (competition_id) REFERENCES competition(id) ) ENGINEInnoDB COMMENT 成绩表;这里有一点很关键比赛项目里测试数据不能随便造。成绩分布要符合真实比赛规律比如一等奖占比 10%二等奖占比 20%还要有不同赛事的历年数据。ECharts 图表好不好看很大程度上取决于 SQL 能不能做出有说服力的统计结果。3.2 后端工程初始化后端使用 Spring Boot MyBatis-Plus可以大幅减少重复代码。pom.xml中需要引入的核心依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意MyBatis-Plus 版本、MySQL 驱动版本需要和 Spring Boot 版本匹配。如果启动时报ClassNotFoundException优先检查依赖版本。配置文件application.yml示例server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_competition?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里要多说一句不要把数据库密码直接写死在代码里。比赛项目虽然规模不大但评委可能会检查配置管理是否规范。推荐使用环境变量覆盖例如password: ${DB_PASSWORD:123456}这样默认密码能跑生产环境又能通过环境变量替换。3.3 成绩导入与统计接口“成绩导入”是比赛项目的高频演示功能也是容易出问题的地方。很多队伍使用 Excel 导入但没考虑数据量大、格式错误、重复导入的情况。这里给出一个 Controller 核心片段RestController RequestMapping(/api/score) public class ScoreController { Resource private ScoreService scoreService; PostMapping(/import) public Result importScore(RequestParam(file) MultipartFile file, RequestParam(competitionId) Long competitionId) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String fileName file.getOriginalFilename(); if (fileName null || !fileName.endsWith(.xlsx)) { return Result.error(仅支持 .xlsx 格式的 Excel 文件); } try { int count scoreService.importFromExcel(file.getInputStream(), competitionId); return Result.success(成功导入 count 条成绩); } catch (Exception e) { log.error(导入成绩失败, e); return Result.error(导入失败 e.getMessage()); } } }实际导入逻辑建议在 Service 中完成并且加上事务控制。如果用 EasyExcel 或 POI 解析要注意表头校验、空行过滤、学号去重。更稳妥的做法是先把 Excel 解析成临时列表调用insertBatch批量插入避免逐条插入导致的性能问题。统计接口是可视化大屏的数据来源例如查询某场赛事的获奖率GetMapping(/statistics/{competitionId}) public Result getStatistics(PathVariable Long competitionId) { MapString, Object result scoreService.getCompetitionStatistics(competitionId); return Result.success(result); }SQL 层面可以使用条件聚合SELECT COUNT(*) AS total_count, SUM(CASE WHEN is_award 1 THEN 1 ELSE 0 END) AS award_count, AVG(score) AS avg_score, MAX(score) AS max_score FROM score WHERE competition_id #{competitionId}比赛现场评委经常会让选手“换一个赛事看看统计结果”如果接口参数设计合理前端只需要切换赛事 ID图表就能联动更新这会成为明显的加分项。4. 前端可视化大屏实现4.1 创建前端工程前端使用 Vue 3 Vite Element Plus创建项目后安装依赖npm create vitelatest frontend -- --template vue cd frontend npm install npm install element-plus element-plus/icons-vue axios echarts这里推荐把接口请求封装到src/api目录不要在组件里直接写axios地址。比如src/api/score.jsimport request from /utils/request export function getScoreStatistics(competitionId) { return request({ url: /api/score/statistics/${competitionId}, method: get }) } export function importScore(data) { return request({ url: /api/score/import, method: post, data }) }封装之后接口地址、请求头、错误处理都统一管理答辩时也更容易解释。4.2 调用后端接口request工具类可以简单封装如下import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:8080, timeout: 10000 }) request.interceptors.response.use( response response.data, error { ElMessage.error(error.message || 请求失败) return Promise.reject(error) } ) export default request跨域问题在前后端分离项目中很容易出现。有三种常见解决方式在后端添加 CORS 配置使用 Nginx 反向代理前端 Vite 配置 proxy。比赛现场最推荐第三种或第二种因为生产部署本来就要用 Nginx顺手把/api代理到后端端口即可。4.3 ECharts 大屏组件ECharts 是可视化大屏的核心。下面是一个简单的获奖率柱状图组件template div refchartRef stylewidth: 100%; height: 400px/div /template script setup import * as echarts from echarts import { ref, onMounted, watch } from vue const props defineProps({ data: { type: Object, default: () ({}) } }) const chartRef ref(null) let chart null function renderChart() { if (!chart) return const awards [一等奖, 二等奖, 三等奖, 未获奖] const values [ props.data.first || 0, props.data.second || 0, props.data.third || 0, props.data.none || 0 ] chart.setOption({ title: { text: 赛事获奖分布 }, tooltip: {}, xAxis: { type: category, data: awards }, yAxis: { type: value }, series: [ { type: bar, data: values, itemStyle: { color: #4f81bd } } ] }) } onMounted(() { chart echarts.init(chartRef.value) renderChart() }) watch(() props.data, renderChart, { deep: true }) /script这个组件看起来很基础但正是这种基础组件决定了比赛演示的稳定性。评委经常会在演示时要求切换赛事如果watch没有写对图表就不会刷新如果onMounted里没有做 DOM 存在性判断页面切换时就会出现Cannot read properties of null的报错。大屏页面建议单独做一个路由使用深色背景和网格布局让图表成为视觉焦点。但不要为了炫酷而引入太多动画比赛现场电脑性能不一定好动画会导致卡顿。5. 部署上线与答辩演示5.1 后端打包部署比赛项目一定要提前演练完整部署流程。后端打包命令mvn clean package -DskipTests打包完成后在target目录下会生成 jar 包。上传到服务器后使用以下命令启动nohup java -jar backend-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 这里需要重点强调日志。比赛现场如果服务启动失败第一件事就是查看app.log。很多队伍在演示时遇到 500 错误却连日志文件都没有只能对着屏幕干着急。建议在项目中至少配置logback-spring.xml把日志按天输出到logs目录这样排查问题会高效得多。5.2 前端构建与 Nginx 配置前端构建命令npm run build构建完成后dist目录就是静态文件。Nginx 配置片段server { listen 80; server_name localhost; root /opt/campus-competition/frontend/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }try_files这一行非常关键它解决的是 Vue Router 在 History 模式下刷新页面 404 的问题。如果比赛现场刷新系统管理页面后白屏多半就是缺少这一行配置。5.3 演示前 Checklist比赛演示环节的稳定性往往比功能本身更重要。以下几个检查项建议打印成 A4 纸贴到工位旁边数据库服务是否已启动测试数据是否完整Redis 服务是否已启动缓存是否预热后端 jar 包是否已通过健康检查接口前端页面是否能正常访问大屏图表是否有数据演示账号是否可用密码是否还记得备用演示浏览器是否提前打开手机热点是否可用防止现场网络故障。很多“全省第三”的遗憾就发生在演示前的 5 分钟。某个服务没启动或者测试数据被人清空了结果整场答辩节奏全乱。6. 比赛现场常见问题排查比赛结束后的复盘里团队会发现自己遇到的每个问题其实都不冷门。下面整理了一份高频问题排查表问题现象常见原因解决思路后端启动失败端口被占用netstat -ano查看端口修改server.portMySQL 连接超时数据库服务未启动检查 MySQL 状态核对连接 URL页面跨域报错前后端地址不一致使用 Nginx 代理或后端配置 CORSExcel 导入失败文件格式不支持校验后缀名使用 EasyExcel 解析大屏图表空白后端返回数据为空检查 SQL 条件确认赛事 ID 是否正确刷新页面 404Nginx 未配置 try_files增加try_files $uri $uri/ /index.html;内存溢出大文件上传或数据量过大调整 JVM 参数控制批量插入数量接口返回 500代码异常且未全局捕获增加全局异常处理器记录日志每支队伍都可以整理一份自己的排错清单。比赛现场没有太多时间现查资料提前把可能遇到的问题写清楚能大幅缩短故障恢复时间。7. 从“全省第三”到一等奖的工程建议7.1 代码规范与版本管理比赛项目通常由 3 到 5 人协作完成如果代码风格不统一后期合并成本会非常高。建议从第一天就坚持以下规范后端包名按controller/service/mapper/entity/config分层前端接口统一放在api目录页面组件放在views目录提交代码时必须写清楚 commit message例如feat: 新增成绩导入功能使用 Git 分支管理主分支保持可运行每个接口都要有注释写明参数含义和返回结果。答辩时评委可能会随机打开一个文件问你“这段逻辑解决什么问题”。如果代码命名规范、注释清晰回答起来会非常从容。这也是失分最少、最容易提升的部分。7.2 安全与数据边界比赛项目虽然不会真实对外提供服务但安全设计也是评分点之一。需要注意几个基础问题用户密码不能明文存储至少使用 BCrypt 加密不要使用拼接字符串方式构造 SQL防止 SQL 注入后端接口要做登录校验不能直接暴露危险操作Excel 上传文件要限制大小防止内存溢出演示数据不能使用包含个人隐私的虚假信息。还有一个常见问题很多队伍把管理员账号密码写死在 README 里并且数据库中的密码是明文。评委看到后虽然不一定会扣分但会认为团队缺乏安全意识。更好的做法是在初始化 SQL 中提供默认账号密码使用 BCrypt 加密后的密文并备注“首次登录后请修改密码”。7.3 文档与答辩准备比赛项目通常要求提交“项目文档”包括需求分析、系统设计、数据库设计、接口文档、测试报告。文档质量直接说明团队的工程化能力。建议在开发结束前三天就开始整理文档不要等最后一天熬夜写。重点写这些内容系统架构图说明前后端如何交互数据库表关系说明解释为什么这样设计核心接口的请求和响应示例部署步骤能够让评委按照文档复现项目项目总结包括遇到的问题和解决方案。答辩准备时团队可以演练“一分钟版本”和“五分钟版本”两套讲解词。一分钟版本用于开场快速抓住评委注意力五分钟版本用于完整演示核心功能。注意控制演示节奏不要在前端页面上纠结太久要把时间留给数据可视化分析和项目难点讲解。8. 结语比赛结束后有人把奖牌挂在书包上也有人把失败页面的截图纸贴在工位上。全省第三的成绩值得开心但更值得记录的是那些在深夜反复排查的异常日志。对下一支参赛队来说真正能带走的不是名次而是一套可复用的项目工程化方法。如果只能给下一届队伍留一句话不要急着写代码先把数据字典、接口文档、部署手册和演示脚本写好。全省第三的遗憾多数不是技术不够而是工程化不足。回去把这次遗憾涉及的每个问题重新跑一遍你会比奖牌更有底气。