
做学生选课系统这两年我前后折腾过好几种技术组合从PHP到Java Spring都试过最后稳定在Node.js Vue这套方案上。身边好几个朋友也在做类似的管理系统经常问我选课系统怎么设计、课程评价模块怎么做、前后端怎么联调。这篇就把我基于Node.js和Vue从零搭起来的学生选课系统加课程评价管理系统的完整思路、核心代码和踩坑记录都摊开讲讲希望能给正在做毕设、课设或者公司内部小系统的同学一点实际参考。这个系统解决的核心问题其实很明确学生要能在线查课、选课、退课老师要能发布课程、查看选课名单、管理评价管理员要能统筹课程数据和用户权限。而课程评价管理这部分很多选课系统都懒得做或者只做个打分但实际上它才是帮助学校持续改进教学质量的抓手。我的做法是把选课和评价做成两个独立又互相关联的模块选完课才能评价评价结果反过来给老师排课和教务决策提供数据支撑。适合来看这篇内容的主要是三类人一是正在做课程设计或毕业设计的学生二是公司里需要快速搭建内部培训选课系统的开发者三是对Vue和Node.js实战还不太熟、想找个完整项目练手的前端新人。我会尽量把我实际写代码时的取舍、踩过的坑、还有那些不跑一遍根本发现不了的问题都交代清楚。1. 项目需求剖析与技术选型思路1.1 选课系统的核心业务与角色权限先别急着写代码把业务理清楚比什么都重要。一个正规的学生选课系统角色一般分为三种学生、教师、管理员有时候还会加一个教务员的角色但通常可以归入管理员。每个角色的权限边界是系统设计的第一道关卡。学生的核心操作是浏览课程列表、查看课程详情包括授课教师、学分、上课时间、容量、已选人数、提交选课、退课以及选课结束后对已选课程进行评价。这里有个容易被忽略的点选课系统不是每学期都在线的它有选课窗口期。我在设计时把选课状态做成了可配置的管理员可以手动开关选课通道而不是写死在代码里。否则每次改选课时间都得重新部署一遍那太痛苦了。教师的权限则围绕课程管理创建课程、编辑课程信息、查看自己课程的选课学生名单、导出名单、查看学生对课程的评价汇总。教师不需要也不能看到其他老师的课程数据。这里用教职工号作为唯一标识来关联数据。管理员是最高权限角色负责管理用户增删改查学生和教师账号、审核课程、管理选课窗口、查看全局统计数据比如每门课的选课人数、热门课程排行、教师平均评分排名。我实际做下来觉得管理员的统计页面是整个系统最出彩的地方因为数据汇总呈现好了能给教务决策提供很多支撑。1.2 为什么选Node.js Vue这对组合这个组合在技术圈已经非常成熟但对于中小型管理系统它算是我用过的性价比最高的方案之一。Node.js用的是JavaScript语言前端Vue也是JavaScript全栈同构语言之后前后端联调的数据结构沟通成本非常低一个人开发的时候不需要在脑子里频繁切换两套语言体系。后端选Node.js里的Express框架主要是因为它轻量、中间件生态丰富、上手门槛低。Express的好处是自由度高不像Spring Boot那样有那么多约定规范项目结构可以按自己的习惯来。对于选课系统这种CRUD占比很高的业务系统Node.js的异步非阻塞I/O模型完全够用即使高峰期几百个学生同时提交选课请求配合数据库连接池也能稳定扛住。当然我这么说不是贬低Java而是说在业务不复杂、又是个人或小团队开发的前提下Node.js的开发和部署效率确实更高。前端选Vue核心原因是它渐进式的学习曲线。Vue对于从零开始的人来说很友好模板语法直观双向绑定写起来不要太爽。选课页面里大量表单交互、课程列表筛选、状态切换这类UI逻辑用Vue的响应式系统可以省掉一大堆手动操作DOM的代码。另外Vue生态里的Vue Router和Pinia或者Vuex都是现成的解决方案配合Element Plus这套UI组件库能快速把后台管理页面的框架搭出来。我实际开发时还用到了一些辅助技术数据库用的MySQL因为大部分学校或企业内部已有的数据都是MySQL方便对接服务器部署用的Nginx反向代理Node.js服务前端构建后的静态文件也由Nginx托管接口测试用的Postman调试前端的时候用Vue Devtools。这套组合拳下来一个人从零到上线两到三周时间是完全可以做到的。2. 环境搭建与开发准备2.1 Node.js环境的安装与版本选择很多新手在这一步就开始卡壳而且卡的方式五花八门。我建议直接上LTS版本也就是长期支持版。不要图新鲜装最新的奇数版本也不要装太老的历史版本因为新版Node.js对ESM的支持更稳定同时兼容当前主流的Express和Vue脚手架。Windows下安装没什么好说的从Node官网下载.msi安装包一路下一步。但装完之后有件事必须做确认npm镜像源。国内默认的npm源速度不稳定我第一周就被这个折磨过装个依赖等半天还各种报网络错误。解决办法是改成国内镜像源一行命令npm config set registry https://registry.npmmirror.com这个镜像源就是之前的淘宝镜像现在改名叫npmmirror了同步频率很高日常开发完全够用。设置完后可以通过npm config get registry验证是否生效。Mac用户建议用Homebrew来装brew install node一行命令搞定后续升级也方便。Linux服务器上最推荐用nvm来管理Node版本因为一台服务器上可能同时跑着多个项目每个项目要求的Node版本不一样nvm可以随时切换避免因为版本不兼容导致部署后跑不起来这种尴尬情况。2.2 npm脚本执行权限问题的终极解法关于这个报错我印象太深了。很多同学在Windows上执行npm命令时终端突然弹出npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。头一回碰见这个很多人直接懵了我明明装好了Node怎么npm就用不了其实这个问题的根源在于Windows PowerShell的默认执行策略是Restricted它禁止运行任何.ps1脚本文件而npm实际上是个脚本文件所以被拦住了。解决办法有三种我按推荐程度排个序。最省事的是以管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned这个策略的含义是本地创建的脚本可以运行从网上下载的脚本必须经过签名才能运行。好处是既解决了npm执行问题又保留了基本的安全防护。执行后输入Y确认然后重启PowerShell窗口npm就能正常用了。第二种办法是永远用命令提示符cmd而不用PowerShellcmd不受这个执行策略的限制。不过这只算权宜之计因为现在很多开发工具比如VS Code的默认终端都内置PowerShell总不至于每次都要手动切终端。第三种办法是如果你用的是VS Code可以直接改它的终端设置把默认终端从PowerShell改成Command Prompt。但这不是根除方案只要你换一台机器或者换个环境问题还是会回来。最建议的做法还是修改执行策略一劳永逸。2.3 Vue脚手架的安装与项目初始化后端和前端最好分开目录管理我的习惯是项目根目录下建server和web两个子目录一个放Node.js后端一个放Vue前端。这样界限清晰部署和调试都方便。前端部分用官方推荐的Vite构建工具创建Vue项目而不是老一代的Vue CLI。Vite的优势是开发服务器冷启动速度快到飞起热更新也是即时的用起来比Webpack时代舒服太多。创建命令npm create vitelatest web -- --template vue这个命令会创建一个使用Vue 3的Vite项目模板。web是项目目录名可以自己改。创建完成后cd web npm install npm run dev这时访问http://localhost:5173就能看到Vue的默认首页了。注意Vite默认端口是5173不是传统的8080。接下来安装路由、状态管理、UI组件库这些常规依赖npm install vue-router4 pinia element-plus axios echartsvue-router4是Vue 3版本对应的路由pinia是Vue 3推荐的状态管理工具比Vuex更简洁element-plus是Element UI的Vue 3版本后台管理界面的利器axios用来发HTTP请求echarts后面做数据统计图表用。我实际开发时踩过一个坑安装Element Plus后按需引入的配置没弄对导致打包体积非常大而且有些组件样式不生效。后来直接用完整引入虽然体积大一点但省心。如果你对性能有要求还是建议用官方推荐的自动按需导入方案unplugin-element-plus后面有空再优化。3. 后端设计与核心接口实现3.1 数据库模型设计一个选课系统的核心表我数了数大概这么几张用户表user、课程表course、选课记录表enrollment、评价表evaluation、学期表semester。还有一张关联教师和课程的表但通常简化成在course表里加教师ID字段。先看用户表。用户表要区分角色所以设计成包含username登录账号、password密码哈希值、realName真实姓名、role角色取值student/teacher/admin、studentNo或teacherNo学号/工号。密码绝对不能明文存储这是底线。我用的bcryptjs做哈希加盐即使数据库泄露了密码也不至于直接裸奔。课程表的字段是重头戏course表包含courseCode课程编号唯一、courseName课程名称、credit学分、teacherId关联用户表、capacity容量上限、schedule上课时间这个字段的设计最复杂因为一门课一周可能有多个时间点我用JSON数组存储比如[{day:1,period:3},{day:3,period:1}]分别表示周一第三节课、周三第一节课、semesterId学期ID、description课程简介、status审核状态。这个schedule的存储方式是我想了很久才定的。一开始老老实实建了一个course_time表搞一对多关系后来发现查询时联表复杂而且实际中课程时间的冲突检测并不需要那么精细。用JSON字段存简化查询冲突判断逻辑也好写不少。当然如果你用的是MySQL 5.7以下版本就不支持JSON字段了建议用文本字段存JSON字符串。这也是为什么我建议直接上MySQL 8的原因。选课记录表最简单三个字段enrollment表包含studentId、courseId、selectedAt选课时间、statusenrolled/withdrawn。这个表要建联合唯一索引(studentId, courseId)防止同一个学生重复选同一门课。评价表的字段设计evaluation表包含enrollmentId关联选课记录保证只能评价自己选过的课、courseId、studentId、score数值评分1到5分、content文本评价、teacherRating可选项对教师的额外评分、createdAt。我在这个基础上又加了一个匿名标识字段保证学生可以放心吐真言。3.2 项目结构与登录认证模块后端我用Express框架项目结构考虑了可维护性。没有用那种全部逻辑堆在一个文件里的写法而是按功能模块划分了路由层、控制器层、数据模型层。核心结构大致如下server/ ├── app.js // 入口文件初始化Express实例 ├── config/ │ └── db.js // 数据库连接配置 ├── models/ │ ├── user.js // 用户数据操作 │ ├── course.js // 课程数据操作 │ └── enrollment.js // 选课数据操作 ├── routes/ │ ├── auth.js // 登录注册路由 │ ├── course.js // 课程相关路由 │ ├── enrollment.js // 选课退课路由 │ └── evaluation.js // 评价路由 ├── middlewares/ │ ├── auth.js // JWT验证中间件 │ └── role.js // 角色权限中间件 └── utils/ └── response.js // 统一响应格式登录用JWTJSON Web Token。流程是用户提交用户名和密码后端校验通过后签发一个token返回前端前端后续请求在请求头里带上Authorization: Bearer token后端中间件解析token取出用户ID和角色放request对象里供后续逻辑使用。JWT的好处是服务端不需要保存session无状态扩展性好。缺点也明显token一旦签发就无法主动吊销除非额外做一个黑名单机制。不过在选课系统这个场景下用户量不大、安全要求也不是金融级别JWT够用了。签发token的关键代码大概是这样const jwt require(jsonwebtoken); function generateToken(user) { return jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 24h } ); }process.env.JWT_SECRET从环境变量里读不要硬编码在代码里不然部署到公网服务器就是给攻击者送欢迎礼物。3.3 选课核心逻辑与冲突处理选课是整个系统最核心的业务这里面的逻辑一不小心就出bug。我完整跑通后整理了四条硬性规则一门课只能选一次、超过容量上限不能选、同一时间段存在课程冲突时不能选、不在选课窗口期内不能选。四条规则缺一不可。并发控制是这里最容易翻车的地方。设想一个场景最后一门课只剩一个名额两个学生同时点击选课如果代码逻辑是先查询剩余容量容量大于0就插入记录那两个人都会通过检查最终超录。解决办法有三种方案。第一种是数据库层面加唯一约束联合索引这样即使两条请求都通过业务检查插入时也会有一条被数据库拒绝。但这样用户体验不好错误信息不够友好前端拿到的报错可能是Duplicate entry。第二种是对课程记录加锁MySQL的行级锁。在事务里SELECT ... FOR UPDATE锁定那门课程的行记录然后检查容量、插入选课记录、更新已选人数最后提交。这个方案能从根本上解决并发问题但要注意事务隔离级别和锁粒度别把整张表锁了。第三种最巧妙在SQL层面做条件更新只有当前已选人数小于容量时才更新同时限制更新影响行数。类似这样UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity如果这个语句影响行数为1说明抢座成功再去插入选课记录如果影响行数为0说明已满直接返回课程已选满。这个方案省去显式事务也避免了锁等待在中小规模系统中性能表现非常好。三种方案里我实际采用了第三种作为核心方案配合第一种作为兜底双重保险。课程时间冲突检测的逻辑也要讲清楚。我设计的schedule字段存的是JSON数组每个元素包含day和period字段比如[{day:1, period:3},{day:3, period:1}]。检测冲突的SQL做法是查询学生已选的所有课程然后在前端或者Node.js里逐个比对时间片是否重叠。因为一个学生一个学期选课数量通常不超过十几门这个比较过程的性能开销完全可以忽略。3.4 课程评价模块的接口设计课程评价模块是我最想展开讲的因为这是选课系统延伸出来的增值功能做得好能给整个项目加分不少。评价流程的核心约束是只有确实选过这门课的学生才能评价且只能评价一次。所以评价接口必须验证enrollment记录是否存在。接口设计如下评价提交接口POST /api/evaluation 请求体: { courseId, score, content, teacherRating }后端处理先根据JWT解析出studentId再查enrollment表确认该学生确实选了这门课如果没选课直接返回403未选课无法评价再查evaluation表确认没有重复评价如果已评价返回请勿重复评价都通过就插入评价记录。查询课程评价接口GET /api/evaluation/course/:courseId后端返回该课程所有评价列表同时附上平均分、各分数段占比。这些统计数据是给教师和管理员看的学生端也能看到但通常是只读的。教师在教师端看到的评价汇总我实现时用了MySQL的聚合查询const result await db.query( SELECT course_id, AVG(score) AS avg_score, COUNT(*) AS count FROM evaluation WHERE course_id ? GROUP BY course_id, [courseId] );这里我踩过一个坑MySQL的AVG函数会返回一个很长的小数比如4.55555直接传给前端很难看。应该在SQL层就做舍入ROUND(AVG(score), 1)或者在Node.js端用Math.round(value * 10) / 10处理再返回。匿名评价的功能也要考虑。有的学校希望评价对教师不可见具体评价人只显示内容有的则无所谓。我通过evaluation表里的anonymous布尔字段控制当为true时教师端查询到的评价列表里学生名字显示为匿名学生。前端根据这个字段做展示区分就够了不用在后端单独另写接口。4. 前端页面与核心交互4.1 前端路由与状态管理设计Vue前端的路由设计直接映射到系统的页面结构。我的路由规划是这样的// 学生端路由 /login // 登录页 /student/dashboard // 学生首页 /student/courses // 可选课程列表 /student/my-courses // 我的已选课程 /student/evaluations // 我的课程评价 // 教师端路由 /teacher/dashboard // 教师首页 /teacher/courses // 我创建的课程 /teacher/course/:id // 课程详情含选课名单和评价汇总 // 管理员端路由 /admin/dashboard // 管理后台首页 /admin/users // 用户管理 /admin/courses // 课程审核与全局管理 /admin/statistics // 数据统计路由守卫是必须做的。我用的是Vue Router的beforeEach全局前置守卫实现思路是如果访问的路径名以/student开头就先检查本地存储的userInfo和token如果没有则重定向到登录页如果有再检查userInfo.role是不是student不是就重定向到403页面。这里有个细节路由守卫里判断角色不能只看前端数据因为前端代码是可以被篡改的。严格的权限控制必须由后端接口配合。前端路由守卫只是体验层面的拦截让普通用户不会看到自己访问不了的功能入口真正的数据安全靠后端的JWT中间件和角色中间件保证。状态管理我用Pinia比Vuex好写很多。核心的store有userStore存token、用户信息、courseStore课程列表缓存、enrollmentStore选课状态。我建议把当前选课窗口是否开启这个全局状态放到store里因为它会被多个页面用到——学生选课页要判断课程详情页要判断个人中心也要判断。避免每个页面单独请求后端接口。4.2 学生选课页面的实现与数据交互学生选课页面是用户最常接触的界面做得好不好直接影响体验。我把它设计成了一个可筛选的课程列表默认按学期筛选支持按课程名称搜索、按学分筛选、按上课时间筛选同时每张课程卡片上展示课程容量条已选人数/总容量和选课按钮。课程列表的数据获取必须用axios封装统一的请求模块。我在utils/request.js里配置了axios实例import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动附带 token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });这里baseURL是/api而不是完整的后端地址http://localhost:3000/api是因为我在Vite里配了代理转发。前端请求/api/coursesVite开发服务器会把请求转发到http://localhost:3000/api/courses。这样解决了开发环境和生产环境的跨域问题前端代码里不用写死后端地址部署时也灵活。Vite代理配置在vite.config.js里server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }选课按钮的交互逻辑要处理好几个状态课程是否可选取决于容量和窗口期、是否已选、当前是否正在请求中防止用户连点两次重复提交。我用button的loading属性处理请求中的状态用户点击后按钮立即变loading请求结束后根据结果刷新按钮状态。这里再分享一个我踩过的坑课程列表数据是接口一次性返回的但选课后的人数是实时变化的。一开始我选完课后直接修改当前页面的数据结果刷新后选课人数还是旧值。后来统一方案是选课成功后调用一次重新获取课程列表的接口用服务端返回的最新数据替换本地数据。虽然多一次请求但保证了数据一致性代价是可以接受的。4.3 教师端课程管理与评价汇总教师端登录后看到的是我创建的课程列表。这个列表的课程卡片上显示已选人数和评价状态重点的是点进课程详情后能看到Tab切换的学生名单和课程评价两个面板。学生名单面板用Element Plus的表格组件展示支持按学号搜索、按状态筛选还有导出功能。导出CSV是教师很需要的功能我实现的时候没用什么复杂插件直接用Node.js后端生成CSV文件前端调接口下载即可。核心代码const csv 学号,姓名,选课时间\n students.map(s ${s.studentNo},${s.realName},${s.selectedAt}).join(\n); res.setHeader(Content-Type, text/csv; charsetutf-8); res.setHeader(Content-Disposition, attachment; filenamestudents.csv); res.send(\uFEFF csv); // BOM防止中文乱码课程评价面板展示的是评价列表和统计图表。统计图表用ECharts平均分用仪表盘图分数分布用饼图。刚开始做的时候我不太熟ECharts花了不少时间后来发现一个规律把数据聚合的逻辑尽量放在后端返回前端只负责把数组传给图表组件这样可以减少前端拼数据的麻烦。接口返回的数据结构就像一个对象数组前端直接拿来用结构清晰。4.4 视频播放与扩展功能设计做系统时你可能会被要求的附加功能我把视频播放这点单独拎出来讲因为它确实能提升项目的完整度。比如课程介绍的视频、老师录制的课程导学这些可以用Vue video.js或原生video标签来播放。网上很多资源是m3u8格式的视频流HLS协议的流媒体。浏览器原生不能直接播放m3u8需要借助hls.js这个库。这个在写课程管理类系统时还是值得掌握的因为我后来发现很多客户上传的视频文件并不都是mp4格式。方法很简单npm install hls.js然后封装一个VideoPlayer组件核心代码import Hls from hls.js; const video ref(null); const play () { if (video.value.canPlayType(application/vnd.apple.mpegurl)) { // 原生支持 HLSSafari video.value.src props.src; } else if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(props.src); hls.attachMedia(video.value); } };自动加载m3u8的机制在Chrome/Edge/Firefox上都能跑Safari走原生分支。这样不管视频文件是mp4、webm还是m3u8一个组件都能搞定。还有一个经常被问到的点vue项目源码怎么发给别人以及怎么把后端端口号隐藏掉。隐藏端口的方法其实就是前面说的Nginx反向代理。生产环境把Nginx监听80或443端口配置location /api转发到Node.js的3000端口这样用户浏览器里只看到http://your-domain/api/xxx根本不知道后端是3000端口。5. 常见问题与排查技巧实录5.1 环境配置常见问题速查表开发过程中环境问题占了我调试时间的相当比例。把这些高频问题整理成速查表你遇到时直接对照处理问题现象根本原因解决方案npm命令报无法加载文件PowerShell执行策略限制管理员身份执行Set-ExecutionPolicy RemoteSignednpm install速度极其缓慢默认源在国外npm config set registry https://registry.npmmirror.comVite启动后端口被占用5173端口被其他进程占用修改vite.config.js里的server.port前端请求接口报跨域错误前端端口和后端端口不一致配置Vite proxy代理或后端启用CORS中间件MySQL连接报ER_NOT_SUPPORTED_AUTH_MODEMySQL 8默认认证插件不兼容创建用户时指定IDENTIFIED WITH mysql_native_passwordElement Plus组件样式失效按需引入配置不完整临时改用完整引入或正确配置unplugin-element-plusMySQL 8的这个认证问题很有迷惑性尤其是本地装的数据库没问题、部署到服务器上突然连不上的情况很常见。我建议统一在创建数据库用户时用IDENTIFIED WITH mysql_native_password BY 你的密码显式指定认证插件同时对Node.js里的mysql2库连接配置里加authPlugins或直接使用mysql2默认的caching_sha2_password支持也能解决问题。5.2 后端接口排查思路接口报错是最常见的调试场景。我的排查顺序是先看浏览器Network面板里请求是否发出、响应状态码是多少再去看Node.js控制台的错误堆栈最后检查数据库当前的数据状态。一个高频问题前端登录后跳转页面刷新一下就回到了登录页。这个其实是token存储位置的问题。如果你在登录成功后把token和用户信息存到了localStorage刷新应该还在如果你存到Pinia的store里刷新页面store就重置了自然会被路由守卫弹回登录页。解决方法是写一个persist插件或者简单点登录成功后把用户信息序列化存到localStorage应用启动时读出来重新填入store。另一个我常被问的问题是选课时提示课程冲突我明明没有选课。这个大概率是schedule字段的格式问题。前端传时间片给后端时如果day和period的类型是字符串而不是数字后端做比较时就会出问题。建议在Node.js端先做一层数据清洗和校验格式不对直接返回400告诉前端日期和节次必须是整数。5.3 前端开发中的典型坑Vue项目开发中我认为最容易让新手崩溃的问题是样式冲突。多个组件里都定义了.card或者.btn这类通用样式类名CSS是全局生效的后引入的样式会覆盖先引入的。Vue的style scoped只是给当前组件的节点加了带hash的data属性来限制选择器范围但如果你在组件里用了子组件scoped样式可能传不到子组件内部。解决样式冲突的两个思路一是给组件根元素加上唯一的自定义类名比如course-card-list、evaluation-dialog避免定义太通用的类名二是善用scoped和深度选择器:deep()在需要穿透到子组件内部的场景使用深度选择器。Vue Router里还有一个藏得很深的坑路由参数变化但页面不刷新。比如从课程A详情跳转到课程B详情URL变了但页面内容没变因为Vue Router默认复用组件实例created生命周期不会再执行。解法是监听路由参数变化或者给router-view加:key$route.fullPath强制重新渲染。我强烈推荐用:key方式简单粗暴不会漏掉任何参数变化。关于Node.js里怎样隐藏post和端口号这也是我在技术社区里看到的高频问题。前端代码是完全无法隐藏的所有能看到的只是Nginx映射后的地址。做法的核心就是前面说的Nginx反向代理把不同路径映射到不同后端服务这样对外暴露的永远只是域名和路径具体端口被完全屏蔽。同时也可以用https证书把传输层加密从网络层面更安全。6. 项目部署与性能优化建议6.1 本地开发与生产部署的环境差异本地开发时前后端分开跑Vite提供热更新一切都很舒适。但部署到服务器的时候就完全不一样了很多项目死在本地能跑服务器跑不起来这最后一关。部署方案我推荐的前后端分离部署是这款先在前端项目执行npm run build生成dist静态目录然后把dist里的文件拷到Nginx配置的静态资源目录后端项目把server整个目录传到服务器安装依赖后使用pm2来守护进程保证Node.js进程崩溃后能自动重启。Nginx的核心配置大概长这样server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /www/student-course/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置里try_files $uri $uri/ /index.html;这行是Vue Router的history模式必需配置。它的作用是当用户刷新页面时Nginx找不到对应的真实文件就回退到index.html由前端路由来处理路径。不配这行的话你点页面里的链接跳转没问题但一刷新就404这个问题非常典型。6.2 数据库与接口性能优化选课系统虽然业务不复杂但在选课高峰期数据库的并发压力还是不小的。我踩过几个性能相关的坑分享一下优化经验。为高频查询字段加索引是第一优先级。我建了course表和enrollment表的核心查询字段索引特别是enrollment(studentId, courseId)的联合索引以及course(semester_id)的索引。这些索引能显著加快选课记录查询和课程列表查询的速度。Node.js层可以做响应缓存。比如课程列表接口如果数据变动不频繁可以在后端做内存缓存设定60秒过期。缓存命中率高了就不用每次查数据库。需要注意缓存与数据一致性的平衡——选课状态变化要实时反映所以选课相关接口不要走缓存只有课程基本信息和评价汇总这种低频变化的数据可以做缓存。前端静态资源打包优化也值得做。Vite构建时默认按路由分包但第三方库的重复引用会变大公共代码块体积。可以用build.rollupOptions.output.manualChunks手动分包把大的第三方库比如echarts、element-plus拆成独立的chunk让浏览器可以长期缓存这些不经常变的内容。另外图片资源建议统一走OSS或CDN不要让Nginx扛所有静态文件。6.3 安全加固与数据备份做管理系统安全这个问题不能心存侥幸。我在这上面吃过亏所以现在写系统时会把安全底线放在功能开发同等的位置。JWT令牌的保管是第一道心理防线。前端把token存在localStorage里如果网站被XSS攻击token可能被窃走。所以我对所有的用户输入做了转义和校验同时给axios响应拦截器里加上全局错误处理遇到401token过期或失效时自动清除本地信息并跳转登录页。后端接口的输入校验也不能省。不要相信前端传过来的任何数据后端必须做基础校验——分页参数要转成数字并限制范围、课程名称长度要截断、评分的取值范围要限制在1到5之间。我习惯用一个极简的校验函数统一处理返回400和具体的错误信息这样前端能精确定位问题。数据备份是很多小型项目的盲区。我建议至少每天导出一份MySQL的SQL备份文件。如果用的是云服务器可以借助云平台自带的备份功能如果是物理机可以写个crontab定时任务0 2 * * * mysqldump -u root -p yourpassword student_course /backup/student_course_$(date \%Y\%m\%d).sql把备份文件同步到异地存储这样即使服务器硬盘挂了数据也不至于丢光。开发这个学生选课系统的整个过程我最大的体会是一个系统能不能经受住真实使用的考验靠的不是功能多么花哨而是那些容易被忽略的边界情况处理得是不是足够细致。比如并发抢课那一瞬间的防超录逻辑比如选课窗口期的状态切换比如评价模块的权限校验这些看似很小的点恰恰是一个项目从能跑到能用的关键分水岭。如果你也在做类似的系统我建议核心逻辑不要抄网上代码哪怕慢一点也要自己把每一条规则想透再落笔。项目源码结构上的讲究和这些细节上的推敲最后都会成为你做下一个项目时实实在在的经验积累。