1. 竞赛组织者的真实痛点:报名信息混乱与到场统计全靠人肉
先说个真实场景。我帮学院做过一次校级程序设计竞赛的组织工作,赛前报名用的是在线问卷+Excel汇总,学生的姓名、学号、班级、参赛组别散落在不同的表格里,有人改名、有人重复提交、有人填错学号,到了赛前核对名单的时候,三个负责人对着两张表来回比对,光对数据就花了大半天。比赛当天更麻烦,签到靠纸质表格,一百多人排队,签完还要人工统计谁到了谁没到,现场乱成一锅粥。
其实高校竞赛场景里,这类问题几乎一模一样的:报名入口分散、数据格式混乱、签到方式原始、赛后统计耗时。我当时就冒出一个念头——能不能用自己熟悉的技术栈,把这个流程整个串起来?于是就有了这套基于Node.js+Vue的高校竞赛项目报名打卡管理系统。
这套系统解决的核心问题有三个维度:
- 报名环节:给学生一个统一、规范的在线报名入口,表单校验前置,收集到的数据直接进数据库,不用再手动整理Excel。
- 打卡环节:比赛现场通过系统扫码或输入学号完成签到打卡,数据实时写入,管理员能随时看到“谁到了、谁没到、来了多少人”。
- 统计环节:比赛结束后,报名数据、打卡数据、缺勤数据一键导出,直接用于学分认定、奖项核对和赛后总结。
无论你是正在做课程设计的学生、刚接触前后端分离开发的新手,还是真的想给学院做个竞赛管理工具的开发者,这套系统的设计思路和实现细节都可以直接拿过去用。下面对技术不怎么熟的同学也别慌,我会把涉及到的环境配置、脚手架搭建、核心模块拆分、接口设计这些内容,尽量讲得具体一些,有些坑我也会专门指出来。
2. 系统功能全景拆解:报名、打卡、统计三条主线
在做任何代码之前,先得把业务理清楚。这套系统的用户角色比较简单,就是两类:学生(参赛者)和管理员(竞赛组织者)。围绕这两个角色,系统被拆成了三条明确的功能主线。
2.1 面向学生的报名端
学生端的功能不复杂,核心是报名和查看状态。
- 竞赛列表页:展示当前可报名的竞赛项目,包括竞赛名称、报名截止时间、参赛组别、报名人数上限。
- 报名表单:填写姓名、学号、学院、专业、联系方式、参赛组别等信息,前端做格式校验,比如学号长度、手机号格式、必填项检查。
- 报名状态:提交后能看到“已报名”或“审核中”状态。管理员设置了审核机制的话,报名不会立刻生效,而是等后台通过后学生才具备参赛资格。
- 个人打卡记录:比赛当天完成打卡后,学生可以查到自己“已完成打卡”、打卡时间等记录。
这一块的难点不在功能本身,而在表单设计和数据校验的严谨性。高校场景下,学生提交的数据质量参差不齐,比如学号少一位、手机号写错、姓名里带空格,这些脏数据如果在入口没拦住,后面统计的时候全都要靠人工返工。所以前端校验一定要做足,后端也要再校验一遍,两层防线缺一不可。
2.2 面向管理员的打卡管理端
管理员端是这个系统的重头戏,也是和普通报名系统拉开差距的地方。
- 竞赛项目管理:创建竞赛、设置报名时间窗口、设置打卡时间窗口、关闭报名。
- 报名审核:对学生的报名申请进行通过或驳回操作,驳回时可以填写原因。
- 打卡管理:查看当前竞赛的报名名单,标记打卡状态。实际使用中,最方便的方式是管理员用手机或电脑打开打卡页面,输入学生学号后自动匹配报名信息并打卡;也可以生成一个二维码,让学生扫码后在手机页面里自行打卡。
- 实时统计看板:显示总报名人数、已打卡人数、未打卡人数、打卡率,这一块用简单的图表插件就能出效果。
- 数据导出:把报名名单、打卡记录导出成Excel或CSV,用于赛后归档。
从数据模型的角度看,整个系统的核心表其实就三张:用户表(学生和管理员同表,用角色字段区分)、竞赛表、报名记录表。打卡的字段可以直接挂在报名记录上,比如check_in_status和check_in_time,不需要单独建一张打卡表,除非你要记录多次打卡(比如初赛、复赛两轮打卡),那才考虑拆分。
2.3 可视化统计与数据导出的价值
很多初学的人容易忽略统计这块,觉得“不就是查个数据库吗”。但实际高校竞赛里,赛后统计往往是负责人最头疼的环节。谁到了、谁没到、谁报名了但没来参赛、哪些人需要补签,这些直接关系到后续的学分认定和诚信记录。
我在系统里做了三个维度的统计:
- 报名趋势统计:按天统计报名人数,方便组织者看报名高峰,评估推广效果。
- 打卡状态统计:当前实到/应到/缺勤的数字和比例,比赛当天实时刷新。
- 组别分布统计:不同参赛组别的报名人数和打卡率,用于赛后分析。
导出功能建议直接放在列表页里,一个“导出Excel”按钮就行。前端用xlsx这个库,后端也可以用exceljs,两者选一个就好。我的习惯是前端导出,因为不走接口、不占后端资源,几十上百人的数据量前端处理绰绰有余。
3. 技术选型的取舍:为什么Node.js + Vue适合这种中后台系统
这一节聊聊选型背后的思考,不是拍脑袋选出来的。
3.1 前后端分离的天然契合
这套系统本质上是典型的前后端分离应用:Vue负责页面渲染和交互,Node.js负责接口和数据处理。前后端分离的好处是分工明确,前端只管界面,后端只管数据,两边通过JSON格式通信。
为什么选Vue而不是React?个人体验是Vue对新手更友好,模板语法直观,单文件组件把HTML、CSS、JS都放在一个文件里,维护起来非常顺手。加上Element Plus这类组件库,表格、表单、弹窗、上传组件都是现成的,开发速度能快一大截。这套系统的界面需求无非就是表格+表单+统计卡片,Vue + Element Plus基本是绝配。
3.2 Node.js生态对快速交付的优势
后端用Node.js,选的是Express框架。Express虽然老,但生态成熟、中间件丰富、文档多,遇到问题搜一下到处都是答案,对于这类业务逻辑不太复杂的管理系统完全够用。
有同学可能会问,为什么不用Spring Boot?如果你的团队本来就熟Java,那Spring Boot当然可以。但对于一个人全栈开发或者小团队快速交付的场景,Node.js有几个不可替代的优势:
- 语言统一:前后端都是JavaScript/TypeScript,一个人切换上下文成本极低,不用在Java和JS两套语法之间横跳。
- 上手门槛低:会写前端的人基本能很快上手Node.js后端,不需要理解JVM、类加载、依赖注入这些概念就能开始写接口。
- 启动快:改完代码重启服务基本秒完成,不像Java项目每次重启还要编译半天。
- 内存占用小:一台低配服务器就能跑起来,适合学院里那种不太宽裕的部署环境。
3.3 数据库选型:MySQL还是MongoDB
很多教程喜欢用MongoDB,因为JavaScript操作文档型数据库很顺手。但我要泼一盆冷水——高校竞赛这种业务,数据关系非常明确:竞赛有报名、报名里有学生、学生有打卡记录,这种强关系型的数据结构,老老实实用关系型数据库最稳妥。
我选的是MySQL。原因很简单:
- 报名记录和学生信息之间有明确的外键关联,需要事务保证数据一致性。
- 导出Excel/CSV时,SQL查询再加工比文档型数据库的聚合操作更直观。
- MySQL在高校里普及率高,就算你不是运维,遇到问题也容易找到人帮忙。
如果是本地练习,直接用SQLite也行,零配置,写个连接就能跑。但既然标题是Node.js+Vue的完整项目,还是建议直接上MySQL,至少把连接池、事务这些基本功练一遍。
3.4 为什么不是Flask、Django、ThinkPHP
Flask和Django是Python系,如果后期要做数据分析方向的扩展(比如把比赛成绩用Python做统计模型),选Python也有道理。ThinkPHP是PHP系,开发效率也很高。这些都是好技术,没有绝对优劣。我这套系统选Node.js的核心理由就一条:你已经在写Vue了,再学一套后端技术栈的成本是翻倍的,Node.js让你把精力集中在业务逻辑上,而不是在两个技术栈之间来回切换。
4. 环境搭建容易翻车的几个细节:Node.js安装、npm权限、Vue脚手架
每次写Node.js相关的文章,评论区总会有人卡在环境搭建这一步。尤其这阵子搜“Node.js安装及环境配置”的人特别多,我就把最常见、最容易翻车的几个点单独拿出来说清楚。
4.1 Node.js版本选择与安装
官网下载安装包,过程本身没什么好说的,一直Next就行。需要注意的是版本选择。
我的建议:不要一上来就追最新版,选LTS(长期支持版)就好。比如现在Node.js 20 LTS就非常稳定,各种依赖兼容性基本没问题。LTS版本的好处是经过大量生产环境验证,生态里的包基本都适配了,后期不容易遇到“这个包不支持这个Node版本”的诡异报错。
下载地址直接去Node.js官网,选择Windows Installer (.msi) 64位版本。安装向导里会包含npm,不用单独装。
4.2 环境变量配置的坑
安装完Node.js后,环境变量一般会被自动写入。但你如果想改npm的全局安装目录(比如不想把全局包装在C盘),就需要手动配置。我的做法是在系统环境变量里新增一个NODE_PATH,指向我自己创建的全局包目录。
想验证环境变量是否生效,打开终端输入:
node -v npm -v两个都能正常输出版本号,说明Node.js和npm已经可以用了。
4.3 npm被禁止运行脚本的报错处理
这阵子搜“npm : 无法加载文件”相关内容的人特别多,这个坑几乎新手中招率百分之百。
报错长这样:
npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本出现这个问题的原因:Windows PowerShell默认的执行策略是Restricted,禁止运行任何脚本文件,而npm在PowerShell里是用shell脚本方式执行的,所以直接被拦了。
解决办法有几种:
方法一,以管理员身份打开PowerShell,执行策略变更命令:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned输入Y确认即可。这是临时改当前用户的执行策略,最安全,推荐优先试这个。
方法二:不想改执行策略,就用cmd(命令提示符)替代PowerShell。打开cmd,npm命令能正常执行。但在VSCode的默认终端里还是PowerShell,所以建议先试方法一。
方法三:以管理员身份运行PowerShell,然后执行Set-ExecutionPolicy Unrestricted,这招比较粗放,直接放开所有脚本。不大建议日常这么干,改了后如果忘了改回来,系统安全策略等于形同虚设。
我自己在跑项目的时候,前前后后在几台电脑上装了Node.js,这个PowerShell报错见得最多。新手建议直接按方法一来,一次性解决,后面不会再报这个错。
4.4 用Vite搭建Vue项目
创建Vue项目,现在主流是用Vite而不是Vue CLI(Webpack)。Vite基于ESModule,冷启动快到离谱,开发体验比Webpack好太多。
创建项目命令:
npm create vite@latest competition-system -- --template vue项目创建完成后:
cd competition-system npm install npm run dev浏览器打开http://localhost:5173,看到Vue默认页面,就说明项目基础环境没问题了。
接着安装我们需要的依赖库:
npm install vue-router@4 pinia element-plus axios这就是整套系统前端的核心依赖:路由、状态管理、UI组件库、HTTP请求库。
后端部分的依赖稍微多一点,在项目根目录建一个server文件夹存放后端代码,然后单独初始化:
mkdir server cd server npm init -y npm install express mysql2 cors jsonwebtoken bcryptjs dotenvexpress:Web框架。mysql2:MySQL驱动,支持Promise语法。cors:解决跨域问题。jsonwebtoken:JWT令牌生成和验证。bcryptjs:密码加密,纯JS实现,不依赖原生编译,跨平台兼容性最好。dotenv:加载.env环境变量文件。
这些依赖基本就是整套系统的全部家当,后面不用再额外装什么大的库了。
5. 后端骨架的搭建逻辑:数据表设计、接口分层和JWT登录
后端是整个系统的中枢。代码结构、接口设计、权限控制,这些看起来是“体力活”,但设计得好不好直接影响后续开发和维护的效率。
5.1 数据库表怎么设计
先看核心三张表:
用户表(users)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 主键 |
| username | VARCHAR(50) | 登录账号,一般用学号 |
| password | VARCHAR(100) | bcrypt加密后的密码 |
| name | VARCHAR(50) | 姓名 |
| student_id | VARCHAR(20) | 学号(可能和username相同,但语义不同) |
| college | VARCHAR(50) | 学院 |
| major | VARCHAR(50) | 专业 |
| role | ENUM('student','admin') | 角色 |
| create_time | DATETIME | 注册时间 |
竞赛表(competitions)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 主键 |
| title | VARCHAR(100) | 竞赛名称 |
| description | TEXT | 竞赛介绍 |
| register_start | DATETIME | 报名开始时间 |
| register_end | DATETIME | 报名截止时间 |
| check_start | DATETIME | 打卡开始时间 |
| check_end | DATETIME | 打卡截止时间 |
| max_participants | INT | 最大报名人数上限 |
| status | ENUM('draft','registering','ongoing','ended') | 竞赛状态 |
报名记录表(registrations)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 主键 |
| competition_id | INT | 关联竞赛表 |
| user_id | INT | 关联用户表 |
| group_name | VARCHAR(50) | 参赛组别 |
| status | ENUM('pending','approved','rejected') | 审核状态 |
| check_in_status | ENUM('unchecked','checked') | 打卡状态 |
| check_in_time | DATETIME | 打卡时间 |
| create_time | DATETIME | 报名时间 |
注意,registrations表里要加一个唯一约束(competition_id, user_id),保证一个用户对一个竞赛只能报一次名,这个在数据库层面就拦住,不能只靠代码判断。
5.2 后端目录结构
后端代码不要全堆在app.js一个文件里,几百行还好,上千行就乱套了。我是这样组织的:
server/ ├── app.js # 入口文件,创建express实例,注册中间件 ├── .env # 环境变量(数据库配置、JWT密钥等) ├── config/ │ └── db.js # MySQL连接池配置 ├── routes/ │ ├── auth.routes.js # 登录注册相关路由 │ ├── competition.routes.js # 竞赛管理相关路由 │ └── registration.routes.js # 报名打卡相关路由 ├── controllers/ │ ├── auth.controller.js │ ├── competition.controller.js │ └── registration.controller.js ├── middleware/ │ ├── auth.middleware.js # JWT验证中间件 │ └── admin.middleware.js # 管理员权限中间件 └── utils/ └── response.js # 统一响应格式这个结构并不复杂,但胜在清晰——路由负责分发请求,控制器负责处理业务逻辑,中间件负责鉴权和权限控制,各司其职。就算后面加功能,往对应目录里加文件就行,不会牵一发而动全身。
5.3 JWT登录认证的实现
登录逻辑其实每家都差不多,但有几个细节值得说。
用户注册后,密码用bcryptjs加密存储,加密的时候要加盐,bcrypt.hashSync(password, 10)这里的10就是盐的轮数,太高会影响性能,太低容易被撞库破解,10是实践里比较均衡的值。
登录成功后签发JWT令牌,载荷里放用户ID和角色,不需要放太多信息,令牌要设置过期时间。我一般设7天,时间太长不安全,太短用户要频繁登录,体验差。
const token = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '7d' } );JWT验证中间件的核心逻辑是:从请求头取Authorization: Bearer <token>,解析验证,通过就把用户信息挂到req.user上,后面控制器里要用就方便了。
const authMiddleware = (req, res, next) => { const header = req.headers.authorization; if (!header) return res.status(401).json({ message: '未登录' }); const token = header.split(' ')[1]; try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { return res.status(401).json({ message: '登录已过期' }); } };这里有个常见问题:前端axios请求要统一带上token。解决办法是在前端封装一个axios实例,用请求拦截器把token塞进请求头。这个我们后面说前端的时候详细展开。
5.4 打卡接口的特殊设计
打卡这个接口是整个系统里最容易被低估的地方。表面看就是“把check_in_status改成checked”,但实际要考虑的问题不少。
首先,打卡时间窗口的判断。打卡必须在竞赛设置的check_start和check_end之间才有效,早于开始时间或晚于结束时间都算无效,这个判断逻辑不能放在前端,必须放在后端——前端传什么时间都能伪造,只有后端系统时间为准。
其次,重复打卡的处理。学生可能因为误操作或者网络卡顿,重试了好几次打卡,接口得做幂等处理。实现方式很简单:打卡前先查一下registrations表里这条记录的check_in_status,如果已经是checked,直接返回成功但不再更新时间,或者返回“您已完成打卡”的提示。
再补充一个业务细节:打卡接口应该先校验用户是否报名并通过审核,否则“根本没报名就去打卡”是会出乱子的。所以打卡接口的完整逻辑链是:验证JWT身份 -> 查询报名记录 -> 校验审核状态 -> 校验打卡时间窗口 -> 更新打卡状态 -> 返回结果。每一步都不能少。
6. 前端页面的实现细节:路由设计、请求封装和组件拆分
前端部分是用户能直接看到、摸到的面儿,用的还是Vue3 + Vite + Element Plus这套组合。我来拆解几个关键点,这些地方设计得不合理,后面写页面代码就会很痛苦。
6.1 Vue Router路由骨架设计
路由结构我分了业务页面和管理页面两套,避免混在一个文件里看不过来。
学生端页面:
const routes = [ { path: '/login', component: Login }, { path: '/register', component: Register }, { path: '/student', component: StudentLayout, meta: { requiresAuth: true, role: 'student' }, children: [ { path: 'competitions', component: CompetitionList }, { path: 'my-registrations', component: MyRegistrations }, { path: 'check-in', component: CheckIn }, ] }, ];管理员端页面:
{ path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'admin' }, children: [ { path: 'competitions', component: AdminCompetitions }, { path: 'registrations', component: AdminRegistrations }, { path: 'statistics', component: Statistics }, ] }路由守卫用beforeEach全局守卫实现,每次跳转前判断:
- 页面是否要求登录(
meta.requiresAuth)。要求登录但没token,跳转登录页。 - 页面是否要求特定角色(
meta.role)。学生去访问管理页,直接重定向到学生首页。
这个环节有个很容易踩的坑:路由守卫里判断用户角色的时候,如果角色信息是从localStorage里取的,用户可能手动改localStorage伪造管理员身份。所以前端路由守卫里的角色判断只是一种体验优化(防止误点、隐藏入口),真正的权限管控必须靠后端的接口校验,后端每个管理员接口都要走admin中间件,两边的校验不能混为一谈。
6.2 Axios请求封装与Token处理
我前端项目里统一封装了一个request.js,核心是两个拦截器。
请求拦截器:每次请求前从localStorage取token,塞进请求头。
service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; });响应拦截器:统一处理错误状态码,比如401表示未登录或登录过期,直接跳转登录页并清除本地token。
service.interceptors.response.use( response => response.data, error => { if (error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } return Promise.reject(error); } );实际开发中,很多功能不生效的问题都出在这里。比如登录接口明明正常,但后续请求一直401,检查一下是不是请求头没带上token;再比如跨域问题,前端报CORS错误,多半是后端没配cors中间件或者服务器nginx没加跨域头。这些问题不至于说是疑难杂症,但确实能把人磨到怀疑人生。
6.3 报名表单的动态校验
报名表单是学生用得最多的页面,体验好不好直接影响第一印象。
我用Element Plus的el-form组件,配置rules规则做校验。比如学号,除了必填,还要正则校验格式:
const rules = { studentId: [ { required: true, message: '请输入学号', trigger: 'blur' }, { pattern: /^\d{10,12}$/, message: '学号格式不正确', trigger: 'blur' } ], phone: [ { required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ] };组别选择用下拉框,可以从后端接口动态拉取,也可以前端写死。我建议后端用配置方式返回组别列表,因为竞赛类型可能每年调整,前端写死的话每次改都要重新发版。
提交按钮做成防重复提交:点击后变成loading状态,并加一个submitting标志,没提交完成之前不允许再次点击。这里处理不好,学生双击两次就可能生成两条报名记录,体验很差。
6.4 打卡页面的设计细节
打卡页面是比赛当天被最多人同时使用的页面,学生对它的要求只有一条:快。
所以我采用的是最实用的设计:页面顶部一个大大的输入框,学生输入学号,回车即完成打卡。页面主体是实时的打卡进度列表,底部显示已打卡人数跟总人数的比例。
但这个操作方式有一个体验隐患:万一有人输入错了别人的学号怎么办?所以打卡成功的提示要非常醒目——大号绿色对勾+姓名反馈,让打卡的人知道自己成功了。后续可以再加一个“撤销打卡”功能,需要管理员权限,遇到误操作可以补救。
如果是在机房用电脑打卡,页面适配要注意:很多学校的电脑分辨率是1366x768,分辨率不高,页面布局一定要在低位分辨率下测试一遍,别在1920x1080的宽屏上开发完就以为万事大吉了。Vue组件加响应式布局是必须的,比如用el-col栅格系统,在不同屏幕宽度下自动调整列数,避免小屏下按钮都挤到屏幕外。
7. 打卡模块里那些容易被忽略的边界情况
打卡功能是整个系统里看起来最简单、实际坑最多的模块。我做了几轮自测之后发现,边界情况是这个模块的重点。
7.1 网络抖动:前端请求超时或失败
比赛现场人多,Wi-Fi不稳是常态。学生提交打卡请求的时候,网络可能瞬间断掉,前端axios默认超时设置是0(不超时),这会导致页面一直转圈,体验极差。
我给打卡接口单独设置了超时时间:
service.defaults.timeout = 15000;15秒内没响应就报错,前端提示“网络繁忙,请确认网络后重试”。这个超时时间不能太长,否则用户会一直等待;也不能太短,服务器并发高的时候处理不过来。
但超时了不代表打卡没成功。比如服务器已经写入成功了,只是响应包在回来的路上丢了,这时候前端显示失败,用户再点一次,后端查记录发现已经打卡,返回“您已完成打卡”,这个其实是正确行为。全靠后端接口的幂等设计兜底。
7.2 代打和漏打怎么处理
高校场景下,“代打卡”是个绕不开的话题。A同学让B同学帮忙刷一下学号打卡,这在技术上很难百分之百防住。我能做的防作弊方案是:
- 打卡成功后记录IP地址,如果同一个IP短时间内大量打卡,后台标记异常。这个方法有局限性,学校网络通常出口是同一个IP,所以仅供参考。
- 比赛当天现场核实身份:管理员用后台“已打卡名单”随机核查,和身份证或学生证核对。这属于管理和技术结合的手段。
漏打的情况也常见:学生报名了但比赛当天忘记打卡,或者来晚了错过打卡窗口。这种一般需要管理员手动补打卡,所以我在管理员后台加了一个“手动补卡”操作,由管理员选择学生后执行。补卡的记录不显示为学生“准时打卡”,而是标记“补卡”,方便赛后数据分析时区分。
7.3 同一时间大量学生同时打卡
比赛结束的瞬间往往迎来打卡高峰——上百人同时点打卡,后端扛不住就会502。解决办法:
- 数据库连接池。
mysql2创建连接池时设connectionLimit,不能太小,我一般设10-15,太小会排队,太大数据库可能扛不住。 - 接口响应时间优化。打卡接口的业务逻辑比较简单,一次UPDATE加一次SELECT,理论上几毫秒就能完成,但如果代码里做了太多重复查询,并发上来就会变慢。打卡接口里我用了事务,是因为要同时更新登记状态和记录打卡时间,逻辑上需要保证原子性。
- 如果真到了上千人同时打卡的程度,那可能需要引入Redis做缓存,把打卡请求先写缓存再异步落库。对于高校竞赛系统,单独部署Redis有点过度设计,我建议用连接池和简单的SQL优化先顶上,真出了问题再扩展。
我在测试阶段用并发工具模拟过200个请求打打卡接口,1秒内全部返回成功,服务器没有明显压力。这说明Express + MySQL处理这个体量的并发完全没问题。
7.4 打卡时间窗口跨天的情况
很多竞赛是下午开始、晚上结束,打卡时间窗口可能横跨两天,比如从某天下午14:00到次日22:00。这个在数据库中就是两个DATETIME,前端在竞赛详情页展示的时候最好明确提示“打卡开始时间/截止时间”,避免学生理解成每天都要打卡。
另外一个时间细节:数据库存储时间默认是UTC还是北京时间。MySQL默认跟系统时区相关,如果你部署的服务器时区是UTC,存进去的时间可能偏差8小时。连接串里要明确指定时区:
DATETIME, 括号内url参数:?useTimezone=true&serverTimezone=Asia/Shanghai这个坑我踩过,开发机本地时间正常,部署到服务器后所有时间都偏了8小时,排查了半天才发现是时区配置的问题。前端页面展示打卡时间时,如果差8小时,学生看着会很困惑,所以务必把时区问题在数据库连接阶段就定下来。
8. 从开发到部署:自检清单和平台上线的经验
系统开发完成,还只是完成了一半。从能跑到能上线提供稳定服务,中间还有一段路要走。
8.1 生产环境部署的两种方式
第一种:传统部署,Node+前端分开跑。
- 前端执行
npm run build,生成dist静态文件,用Nginx托管。 - 后端放到独立端口比如3000,Nginx配置反向代理,把
/api路径转发到http://localhost:3000。
Nginx配置参考:
server { listen 80; server_name your-domain.com; root /var/www/competition-system/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }注意try_files那行是Vue Router history模式必需的配置,如果漏了,刷新页面时会出现404——前端路由在服务端不存在对应路径,要被重定向回index.html。
第二种:用Docker Compose一键编排前端+Nginx+后端+MySQL。这种方法的好处是迁移部署方便,换一台服务器几条命令就能把整套环境拉起来。如果你平时接触Docker不多,暂时不用急着上,搞清楚传统部署方式更重要,Docker可以作为进阶学习方向。
8.2 数据备份的重要性
高校竞赛系统的数据量不大,但每一条数据都关系到学生的报名资格和比赛记录,丢了就是事故。我是这样处理的:
- MySQL开启binlog,这样能在出问题的时候做时间点恢复。
- 写一个简单的定时任务脚本,每天晚上把数据库dump成SQL文件,保留最近7天的备份。
- 备份文件下载到本地留底,避免服务器磁盘故障时备份也跟着没了。
这些东西虽然不起眼,但真遇到硬盘挂了、误删了数据的情况,就能深刻体会到备份的价值。我见过不止一个项目上线很久,数据库丢了才发现从来没做过备份,那才叫真没地方哭。
8.3 上线前的自检清单
我把这个清单放在项目里直接当成文档,每次发版前过一遍,这里也分享出来:
- Node环境变量和生产环境配置是否分离,
.env里是否有硬编码的敏感信息。 - 数据库密码是否是强密码,默认的root密码一定改掉。
- 接口是否所有该鉴权的地方都加了鉴权中间件,管理员接口是否校验收了admin角色。
- 前端勿将console.log输出到生产环境,构建时按需去掉调试代码。
- Nginx的
try_files是否配置正确,页面刷新是否404。 - 生产服务器防火墙只放行80/443端口,后端3000端口不对外暴露。
- 打卡功能的时间窗口配置和比赛时间是否一致。
- 导出的Excel文件中文列名和编码是否正常,打开后是否有乱码。
清单每一项背后都对应着一个真实翻车场景。比如导出Excel乱码,是因为前端库默认生成的CSV是UTF-8编码,而Excel打开需要带BOM的UTF-8或GBK编码,加个\ufeff前缀就解决。
8.4 系统后续可以怎么扩展
这套系统的核心骨架搭完之后,扩展方向其实很清晰:
- 数据可视化增强:接入ECharts,把统计看板从数字变成趋势图、饼图、柱状图,按组别、学院、时间多个维度下钻分析。
- 消息通知:报名审核通过或驳回,学生需要一个渠道接收结果。可以接入企业微信应用消息或者邮件通知,个人开发者实现成本低、又免去学生装了多个App的麻烦。
- 扫码打卡:如果比赛现场有足够条件(打印二维码、学生有手机),可以用
qrcode库生成每个学生的专属二维码,管理员扫码完成打卡,可以部分规避代打问题。 - 证书生成:比赛名次出来之后,给获奖学生批量生成电子证书,前端打印模板或后端生成PDF,能省去大量的老式手工填写证书环节。
按我个人的经验,一个系统做到能用是一回事,做到团队真正愿意持续用是另一回事。上线初期我一定会守在后台看数据——看报名转化率、看打卡高峰时长、看哪些学院的学生使用有障碍,再根据这些反馈快速调整功能和交互细节。竞赛管理这类工具,最大的价值不在于功能有多炫,而在于它确确实实帮组织者省了时间、减了焦虑,让几百人的活动在流程上井然有序。到了比赛当天,看到打卡数据实时跳动、统计数字自动汇总,那感受比写一万行代码都踏实。