拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Node.js+Vue的高校竞赛报名打卡管理系统开发实战

基于Node.js+Vue的高校竞赛报名打卡管理系统开发实战

1. 竞赛组织者的真实痛点:报名信息混乱与到场统计全靠人肉

先说个真实场景。我帮学院做过一次校级程序设计竞赛的组织工作,赛前报名用的是在线问卷+Excel汇总,学生的姓名、学号、班级、参赛组别散落在不同的表格里,有人改名、有人重复提交、有人填错学号,到了赛前核对名单的时候,三个负责人对着两张表来回比对,光对数据就花了大半天。比赛当天更麻烦,签到靠纸质表格,一百多人排队,签完还要人工统计谁到了谁没到,现场乱成一锅粥。

其实高校竞赛场景里,这类问题几乎一模一样的:报名入口分散、数据格式混乱、签到方式原始、赛后统计耗时。我当时就冒出一个念头——能不能用自己熟悉的技术栈,把这个流程整个串起来?于是就有了这套基于Node.js+Vue的高校竞赛项目报名打卡管理系统。

这套系统解决的核心问题有三个维度:

  • 报名环节:给学生一个统一、规范的在线报名入口,表单校验前置,收集到的数据直接进数据库,不用再手动整理Excel。
  • 打卡环节:比赛现场通过系统扫码或输入学号完成签到打卡,数据实时写入,管理员能随时看到“谁到了、谁没到、来了多少人”。
  • 统计环节:比赛结束后,报名数据、打卡数据、缺勤数据一键导出,直接用于学分认定、奖项核对和赛后总结。

无论你是正在做课程设计的学生、刚接触前后端分离开发的新手,还是真的想给学院做个竞赛管理工具的开发者,这套系统的设计思路和实现细节都可以直接拿过去用。下面对技术不怎么熟的同学也别慌,我会把涉及到的环境配置、脚手架搭建、核心模块拆分、接口设计这些内容,尽量讲得具体一些,有些坑我也会专门指出来。

2. 系统功能全景拆解:报名、打卡、统计三条主线

在做任何代码之前,先得把业务理清楚。这套系统的用户角色比较简单,就是两类:学生(参赛者)和管理员(竞赛组织者)。围绕这两个角色,系统被拆成了三条明确的功能主线。

2.1 面向学生的报名端

学生端的功能不复杂,核心是报名和查看状态。

  • 竞赛列表页:展示当前可报名的竞赛项目,包括竞赛名称、报名截止时间、参赛组别、报名人数上限。
  • 报名表单:填写姓名、学号、学院、专业、联系方式、参赛组别等信息,前端做格式校验,比如学号长度、手机号格式、必填项检查。
  • 报名状态:提交后能看到“已报名”或“审核中”状态。管理员设置了审核机制的话,报名不会立刻生效,而是等后台通过后学生才具备参赛资格。
  • 个人打卡记录:比赛当天完成打卡后,学生可以查到自己“已完成打卡”、打卡时间等记录。

这一块的难点不在功能本身,而在表单设计和数据校验的严谨性。高校场景下,学生提交的数据质量参差不齐,比如学号少一位、手机号写错、姓名里带空格,这些脏数据如果在入口没拦住,后面统计的时候全都要靠人工返工。所以前端校验一定要做足,后端也要再校验一遍,两层防线缺一不可。

2.2 面向管理员的打卡管理端

管理员端是这个系统的重头戏,也是和普通报名系统拉开差距的地方。

  • 竞赛项目管理:创建竞赛、设置报名时间窗口、设置打卡时间窗口、关闭报名。
  • 报名审核:对学生的报名申请进行通过或驳回操作,驳回时可以填写原因。
  • 打卡管理:查看当前竞赛的报名名单,标记打卡状态。实际使用中,最方便的方式是管理员用手机或电脑打开打卡页面,输入学生学号后自动匹配报名信息并打卡;也可以生成一个二维码,让学生扫码后在手机页面里自行打卡。
  • 实时统计看板:显示总报名人数、已打卡人数、未打卡人数、打卡率,这一块用简单的图表插件就能出效果。
  • 数据导出:把报名名单、打卡记录导出成Excel或CSV,用于赛后归档。

从数据模型的角度看,整个系统的核心表其实就三张:用户表(学生和管理员同表,用角色字段区分)、竞赛表、报名记录表。打卡的字段可以直接挂在报名记录上,比如check_in_status和check_in_time,不需要单独建一张打卡表,除非你要记录多次打卡(比如初赛、复赛两轮打卡),那才考虑拆分。

2.3 可视化统计与数据导出的价值

很多初学的人容易忽略统计这块,觉得“不就是查个数据库吗”。但实际高校竞赛里,赛后统计往往是负责人最头疼的环节。谁到了、谁没到、谁报名了但没来参赛、哪些人需要补签,这些直接关系到后续的学分认定和诚信记录。

我在系统里做了三个维度的统计:

  1. 报名趋势统计:按天统计报名人数,方便组织者看报名高峰,评估推广效果。
  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 dotenv
  • express:Web框架。
  • mysql2:MySQL驱动,支持Promise语法。
  • cors:解决跨域问题。
  • jsonwebtoken:JWT令牌生成和验证。
  • bcryptjs:密码加密,纯JS实现,不依赖原生编译,跨平台兼容性最好。
  • dotenv:加载.env环境变量文件。

这些依赖基本就是整套系统的全部家当,后面不用再额外装什么大的库了。

5. 后端骨架的搭建逻辑:数据表设计、接口分层和JWT登录

后端是整个系统的中枢。代码结构、接口设计、权限控制,这些看起来是“体力活”,但设计得好不好直接影响后续开发和维护的效率。

5.1 数据库表怎么设计

先看核心三张表:

用户表(users)

字段类型说明
idINT 自增主键
usernameVARCHAR(50)登录账号,一般用学号
passwordVARCHAR(100)bcrypt加密后的密码
nameVARCHAR(50)姓名
student_idVARCHAR(20)学号(可能和username相同,但语义不同)
collegeVARCHAR(50)学院
majorVARCHAR(50)专业
roleENUM('student','admin')角色
create_timeDATETIME注册时间

竞赛表(competitions)

字段类型说明
idINT 自增主键
titleVARCHAR(100)竞赛名称
descriptionTEXT竞赛介绍
register_startDATETIME报名开始时间
register_endDATETIME报名截止时间
check_startDATETIME打卡开始时间
check_endDATETIME打卡截止时间
max_participantsINT最大报名人数上限
statusENUM('draft','registering','ongoing','ended')竞赛状态

报名记录表(registrations)

字段类型说明
idINT 自增主键
competition_idINT关联竞赛表
user_idINT关联用户表
group_nameVARCHAR(50)参赛组别
statusENUM('pending','approved','rejected')审核状态
check_in_statusENUM('unchecked','checked')打卡状态
check_in_timeDATETIME打卡时间
create_timeDATETIME报名时间

注意,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,能省去大量的老式手工填写证书环节。

按我个人的经验,一个系统做到能用是一回事,做到团队真正愿意持续用是另一回事。上线初期我一定会守在后台看数据——看报名转化率、看打卡高峰时长、看哪些学院的学生使用有障碍,再根据这些反馈快速调整功能和交互细节。竞赛管理这类工具,最大的价值不在于功能有多炫,而在于它确确实实帮组织者省了时间、减了焦虑,让几百人的活动在流程上井然有序。到了比赛当天,看到打卡数据实时跳动、统计数字自动汇总,那感受比写一万行代码都踏实。

返回列表