ARTICLE DETAIL

资讯详情

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

Node.js+Vue前后端分离的选课缴费管理系统开发实战

Node.js+Vue前后端分离的选课缴费管理系统开发实战 把一套教务选课缴费管理系统完整跑通之后我最大的感受是这类系统的核心从来不是页面多漂亮而是选课名额、缴费状态这些业务数据在各种边界条件下不出错。项目代号ux52l技术栈明确是Node.js Vue一个典型的前后端分离架构。学生可以在线查看课程、报名选课、缴纳学费管理员能维护课程和选课名单财务能处理订单和对账。这套项目非常适合做毕业设计、课程设计或者作为中小型学校教务系统的起步版本。下面我会从需求拆解、环境配置、后端接口、前端交互、问题排查到部署优化把整个项目里值得复用的经验一次写清楚。文章不会只堆代码更多会讲清楚每一步为什么这么做以及实际开发中容易踩到哪些坑。1. 项目需求与整体设计思路1.1 业务需求拆解选课、缴费、管理三个核心场景选课缴费管理系统刚一听好像只是把Excel表格搬到网页上真做起来会发现业务逻辑有不少细节。先从角色看学生要完成注册登录、浏览课程、选课退课、支付订单、查看个人课表教务管理员要维护课程库、设置开课学期、调整课程容量、处理特殊情况财务人员要查看缴费清单、核对订单状态、生成基础报表。三类角色对同一套数据有不同的操作权限这直接决定了后端接口和前端路由都要做角色控制。核心业务流是管理员发布课程、学生选课、系统生成缴费单、学生支付、教务确认名单。这个流程看似线性但会产生大量分支选课时课程冲突怎么办课程已满怎么办学生退课之后已经缴的费用怎么处理支付回调迟迟不到怎么办这些边界问题才是项目质量的关键。我建议开发前先画一张状态流转图把选课记录和缴费单的状态字段定义清楚后面写代码会省很多事。对应到功能列表可以拆成四块用户认证、课程与选课、订单与缴费、报表统计。用户认证管登录注册和权限课程与选课管课程CRUD和冲突校验订单缴费管生成订单、模拟支付、退款统计报表管课程选课人数、收入汇总等简单图表。第一版不用贪多把这四块做扎实就已经超过不少同类项目。1.2 技术选型为什么用 Node.js Vue 而不是传统一体式方案为什么用Node.js Vue原因很简单前后端语言统一。项目主要开发者如果是前端方向Node.js的JavaScript语法和心理负担都很低不用在Java和JS之间来回切换。Node.js处理选课这种I/O密集型任务也有优势尤其选课高峰期有成百上千个并发请求基于事件循环的异步模型配合数据库连接池能扛住中等规模的压力。Vue生态在高校教务系统里也很常见。Vue Router解决页面跳转和权限路由Pinia管理登录状态和选课数据Element Plus提供现成的表格、表单、弹窗组件开发效率非常高。相比传统JSP模板方案Vue的组件化让选课列表、订单卡片这类可复用UI可以抽成组件后期维护也方便。当然也要承认Node.js的边界。如果你的学校已经有完整的Java技术栈和运维体系或者系统需要深度对接财务ERP那Spring Boot可能更合适。但就这个项目而言Node.js MySQL Vue已经足够而且部署成本低一台2核4G的云服务器就能跑得很舒服。1.3 项目整体架构前后端分离如何落地整个项目采用前后端分离架构。前端是标准Vue单页应用开发时通过Vite dev server跑在5173端口所有/api前缀的请求都代理到后端3000端口。后端是Node.js Express也可以换成Koa负责所有业务逻辑和数据存储。数据库使用MySQL选课过程中的行锁和事务都依赖它。如果访问量再大可以在后端和数据库之间加Redis用来缓存课程列表、控制选课并发但第一版建议先不加避免过度设计。项目目录结构我习惯这样组织client/ # Vue前端 src/router/ # 路由配置 src/store/ # 状态管理 src/api/ # 请求封装 src/views/ # 页面 server/ # Node后端 app.js # 服务入口 routes/ # 路由控制器 services/ # 业务逻辑 models/ # 数据库操作 middlewares/ # 鉴权、日志这样组织的好处是职责清晰前端每个人负责自己的视图后端只要保证API稳定。同时数据库连接、鉴权逻辑都收拢在middlewares里不会散落到各个路由。2. 环境准备与基础配置2.1 Node.js 安装与环境变量配置细节先解决环境问题。Node.js版本我建议直接下官网LTS版不用追最新生产环境追求稳定。Windows安装时注意安装目录尽量不要带空格和中文比如默认的C:\Program Files\nodejs\其实带空格大部分情况没影响但个别工具解析会有问题。我自己的习惯是装到D:\dev\nodejs\并手动配置环境变量。安装时勾选Add to PATH安装完打开新终端窗口验证node -v npm -v看到版本号就说明node和npm都可用。npm是随Node一起装的包管理器后续安装项目依赖全靠它。如果出现npm 不是内部或外部命令多半是PATH没配上。需要手动添加Node.js安装目录和全局模块路径到系统环境变量新建NODE_HOME指到安装目录PATH里加%NODE_HOME%和%NODE_HOME%\node_global。另外Vue项目执行npm install时国内网络可能会很慢可以换成国内镜像源。不要全局安装太多不必要的东西项目依赖尽量写在package.json里。2.2 Vue 项目创建与依赖安装破解 npm.ps1 被禁止的坑创建Vue项目我推荐用Vite速度快、配置少。执行npm create vuelatest或者用Vue CLInpm install -g vue/cli vue create client两种方式都可以。这里有一个Windows用户大概率会踩的坑在PowerShell里执行npm突然报错npm.ps1无法加载文件因为在此系统上禁止运行脚本。这个不是npm坏了而是PowerShell执行策略默认禁用了脚本。解决方法以管理员身份打开PowerShell运行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser选择Y确认。这样当前用户就能运行本地脚本安全性也不至于完全放开。如果你不想改执行策略可以用cmd窗口或者Git Bash跑npm命令也能绕过去。这个坑花不了两分钟但第一次遇到真的会懵我把它写在前面。依赖安装完成之后在client目录启动npm run dev看到Local地址就说明前端起来了。默认端口是5173如果被占用Vite会自己换端口。2.3 前后端目录结构从零搭建一个规范仓库做这类项目我习惯把前后端拆成两个独立根目录而不是混在一个package.json里。好处是各自依赖清晰部署时也能单独打包上传。根目录下client和server并列分别放前端和后端代码。server内部再按路由、服务、模型、中间件分层。前端client内部views目录放页面组件router放路由表store放状态api放所有请求函数。这里有一个实用约定所有API请求不直接在组件里写axios而是统一封装在api目录比如user.js、course.js、payment.js每个文件导出一组函数。这样组件代码干净接口变更时只改一个文件。3. 后端核心功能与 API 实现3.1 数据库设计四张表如何支撑选课缴费全流程数据库是这类系统最容易埋雷的地方。选课系统至少要有四张表用户表、课程表、选课表、缴费表。其中选课表和缴费表是核心。下面是一个实际用过的表结构参考字段加得比较克制够用为主。表名关键字段说明usersid, username, password_hash, role, student_no, real_namerole区分学生/管理员/财务coursesid, course_code, course_name, teacher, credit, capacity, selected_count, semester, scheduleschedule存时间地点描述selectionsid, user_id, course_id, semester, status, created_at唯一索引(user_id, course_id, semester)paymentsid, order_no, user_id, selection_id, amount, status, pay_time, created_atorder_no唯一密码不要存明文用bcrypt加密后存password_hash。课程表里的selected_count是个冗余字段维护时要注意和选课表实际数量保持一致后面会在选课事务里同步更新。schedule字段我直接存一长串文本比如“周一第3-4节|综合楼A201”做冲突检测时解析字符串这只适合小型系统如果要支持复杂排课建议拆成星期、节次、教室三个字段甚至单独一张排课表。选课表的唯一索引非常关键。(user_id, course_id, semester)的组合唯一约束能从数据库层面防止同一个学生同一学期重复选同一门课比只靠应用层判断可靠得多。缴费表和选课表通过selection_id关联这样一笔缴费对应一条选课记录退课时能反查是否已缴费。3.2 登录鉴权与权限控制JWT 中间件实战登录接口流程前端把用户名密码POST到/api/auth/login后端先查用户再用bcrypt.compare校验密码通过后生成JWT返回。JWT里包含userId、username、role、expire等字段有效期我设置成24小时时间太长不安全太短又要频繁登录。生成JWT的密钥必须放在服务端环境变量里不要硬编码到代码仓库。鉴权中间件大概长这样const jwt require(jsonwebtoken); function auth(req, res, next) { const token req.headers.authorization?.split( )[1]; try { const payload jwt.verify(token, process.env.JWT_SECRET); req.user payload; next(); } catch (err) { res.status(401).json({ code: 401, message: 未登录或登录已过期 }); } }还需要一个角色中间件function requireRole(...roles) { return (req, res, next) { if (!roles.includes(req.user.role)) { return res.status(403).json({ code: 403, message: 没有权限 }); } next(); }; }使用时这样挂router.post(/courses, auth, requireRole(admin), createCourse);每个接口的权限一目了然。我踩过的一个坑是token里的角色信息被反复读取每次都要查数据库后来直接JWT里带上role中间件直接解析性能更好。坏处是用户角色变更后旧token仍然有效但这类系统角色变动很少可以接受。3.3 选课与退课接口并发冲突和名额限制的取舍选课接口是系统里并发压力最大的业务。逻辑上要做这几件事校验课程存在且当前学期可选校验学生未选过该课校验时间不冲突校验剩余名额大于0插入选课记录并减少名额。这里最大的坑是并发。假设只剩最后一个名额两个学生同时点击选课如果代码先SELECT查名额再INSERT两个请求都可能查到名额还有1最终都执行成功名额变负数据库还不会立刻报错。解决方法是把整个选课过程放在事务里对课程行加锁START TRANSACTION; SELECT * FROM courses WHERE id ? FOR UPDATE; -- 检查capacity和selected_count UPDATE courses SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; INSERT INTO selections (user_id, course_id, semester, status) VALUES (?, ?, ?, selected); COMMIT;UPDATE语句里的AND selected_count capacity是最后一道保险如果影响行数为0就说明名额已满直接抛异常回滚。再配合选课表的唯一索引并发重复选也能被数据库拦截。这套组合拳虽然简单但能处理绝大多数选课场景。退课接口稍简单删除选课记录并把课程selected_count减1。如果这条选课记录已经关联了缴费单就还要处理退款状态不建议自动退款而是把缴费单标记成待退款由财务人工确认避免资金风险。3.4 缴费流程接口订单、支付回调与对账选课成功后系统会自动创建一条缴费单。订单号生成规则是时间戳加三位随机数中间拼上用户ID保证唯一。缴费单状态有pending、paid、cancelled、refunded。金额怎么定简单做法是课程学分乘以每学分价格然后在课程表里加price字段或者在单独参数表里配置学分单价。学生发起支付时前端点击按钮调用/api/payments/{orderNo}/pay后端生成一个支付二维码或跳转链接。对接真实支付需要商户号和回调验签开发阶段我建议先把模拟支付做通提供一个/dev/pay接口把订单状态直接改成paid回调逻辑和真实接口保持一致。上线前再替换成支付网关。支付回调的幂等性必须处理。支付平台会多次通知同一个支付结果如果后端每次都修改订单状态就可能把已支付订单覆盖成初始状态。解决方案是加一个状态判断订单已经是paid直接忽略重复通知。幂等逻辑写好后对账压力会小很多。4. 前端 Vue 页面与交互实现4.1 路由设计与页面划分不同角色看到不同菜单前端路由用Vue Router。先把所有页面按角色和功能划分清楚登录页/login学生端有/dashboard个人课表、/courses选课中心、/payments缴费中心管理员端有/admin/courses课程管理、/admin/students学生管理、/admin/stats统计财务端有/finance/orders订单核对。页面多但结构清晰不同角色入口完全不同。路由表里用meta信息标记权限{ path: /admin/courses, component: () import(/views/admin/CourseManage.vue), meta: { requiresAuth: true, roles: [admin] } }在全局前置守卫里做校验router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) return next(/login); const role localStorage.getItem(role); if (to.meta.roles !to.meta.roles.includes(role)) return next(/); next(); });很多新手会把所有页面放成一堆平铺路由结果同一个页面在不同角色里还要写两个组件。我习惯把学生端和管理端拆开虽然组件有重复但权限隔离更清晰维护成本反而低。4.2 状态管理与请求封装Pinia Axios 的组合前端状态用PiniaVue3项目。主要存两个storeuserStore存token、用户信息、角色courseStore存当前学期的课程列表和已选课程ID集合。为什么要把选课状态放store而不是每个页面自己拉因为选课中心、个人课表、缴费中心都需要显示已选课程如果不共享切页面就要重新请求体验会很差。Axios封装是标准操作。创建实例时设置baseURL请求拦截器从userStore取token塞到header响应拦截器统一处理业务错误如果后端返回401就清掉本地登录状态并跳转登录页。后端接口统一返回{ code, message, data }前端就能在拦截器里做判断组件里只需要处理data。这里有一个经验不要把响应拦截器做得太重。我见过有人把所有接口的报错提示都在拦截器里用Message提示结果部分接口业务失败时页面还想做特殊处理反而被拦截器挡住。更好的做法是拦截器只处理网络错误和401业务错误码交给调用方决定。4.3 选课中心界面课程列表、视频预览与即时冲突提示选课中心是学生最常用的页面。我用Element Plus的表格加卡片模式表格展示课程编号、名称、教师、学分、容量、已选人数行操作根据状态显示选课或退课按钮。页面顶部有学期筛选和课程名称搜索。前端拿到课程列表后还要根据已选课程ID集合判断按钮是否禁用已经选过的课显示“已选”名额满的显示“已满”时间冲突的最好也在点击前做一次本地提示。课程详情弹窗里可以集成视频预览。部分学校会上传课程介绍视频格式多半是HLS的m3u8文件。Vue里可以用hls.js封装一个播放组件核心代码就是加载m3u8地址、绑定video元素。注意两点一是m3u8的请求通常需要带鉴权headerhls.js支持自定义loader二是生产环境要处理跨域和防盗链最省事的是把视频文件放在和前端同域的静态资源目录下。这个功能不是系统的核心但加上之后选课中心会真实很多。选课接口的调用要防止重复点击。按钮点击后先进入loading状态等接口响应恢复。如果后端返回课程冲突或名额不足用消息框提示。我还会在store里维护一个状态同一门课正在提交中时立即禁用按钮避免连续请求导致数据异常。4.4 缴费中心界面订单倒计时与支付状态轮询缴费中心的重点是订单状态和倒计时。选课成功后跳转到订单确认页显示课程信息、金额、订单号并创建一个15分钟倒计时。倒计时结束时如果订单仍未支付前端置灰支付按钮提示订单已过期后端也会有定时任务或惰性检查把超时订单置为关闭。模拟支付页面可以很简单点击支付按钮后调后端/api/dev-pay接口把订单标记为paid同时启动轮询查询订单状态。为什么用轮询而不是等接口直接返回真实支付中支付结果由异步回调通知开发阶段养成轮询的习惯后面接真实渠道会更顺手。轮询间隔我设置3秒一次最多轮询10次超过时间仍未支付则结束轮询。支付完成之后前端要根据订单状态更新本地store里的缴费状态并刷新个人课表。如果是真实支付记得在回调里更新pay_time并且给订单号建立唯一索引防止重复单号。5. 常见问题与排查技巧实录5.1 Windows 下 npm.ps1 无法加载脚本权限策略怎么调这个问题在环境配置章节写过这里再系统整理一次。报错信息类似无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。本质是PowerShell的ExecutionPolicy策略。解决方案有两种。第一种管理员执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser然后重开终端。第二种改用cmd或Git Bash执行npm命令。另外在VSCode终端里也可以切换默认终端从PowerShell到Command Prompt右键终端标签就能换。这个坑和Node本身无关但很多新手会误判。查看当前执行策略用get-ExecutionPolicy -List。RemoteSigned策略的含义是本地脚本可以运行从网络下载的脚本必须有受信任的签名。日常开发这样配置是比较合理的选择。如果公司安全策略不允许改那就用备用方案。5.2 跨域请求失败开发环境代理与 CORS 双保险前后端分离最绕不开的就是CORS。开发时前端在5173端口后端在3000端口浏览器会拦截跨域请求。通常两种处理第一在Vite配置proxy把/api请求代理到http://localhost:3000这样浏览器看到的还是同源第二后端使用cors中间件直接开放跨域。我更推荐开发时代理因为和生产环境Nginx反代的行为一致也能顺便隐藏后端端口。如果你选择后端cors配置要精细const cors require(cors); app.use(cors({ origin: http://localhost:5173, credentials: true }));不要直接app.use(cors())等于允许所有来源线上会非常危险。生产环境如果前端和后端通过Nginx同一个域名部署CORS就不需要了Nginx把/api转发给Node服务即可。5.3 并发选课导致名额超卖事务锁和唯一索引的组合拳前面已经讲了事务锁和唯一索引的方案这里谈谈排查视角。如果上线后发现选课人数超过课程容量多半是代码没有在事务里加锁或者UPDATE条件里漏了selected_count capacity。另一种可能你用了ORM但事务没有正确扩散到所有写操作比如更新课程和插入选课不在同一个事务里。排查时可以临时在UPDATE语句后打印affectedRows看是否出现0行更新却被忽略的情况。数据库连接池也是一个隐藏点。事务期间千万不要把数据库连接释放掉否则后续操作会在另一个连接上执行事务就等于断了。用Express开发时我会用一个getConnection函数从连接池取出连接手动beginTransaction最后commit或rollback始终复用同一个连接。这条路第一次写会嫌麻烦但并发场景下必须这么写。5.4 接口排查三板斧统一返回结构、日志和网络面板后端接口出问题我一般按三个顺序查。第一看网络面板前端F12打开Network看接口请求URL、请求方法、请求体、响应状态码。很多问题一眼就能看出是404还是500或者响应数据结构和预期不符。第二看后端日志我在每个路由都加了日志中间件输出请求路径、耗时、错误堆栈。如果没日志临时用console.log也凑合。第三看数据库状态比如选课超卖直接查selections表里有没有重复记录再看courses表的selected_count是否对得上。同时接口返回结构一定要统一。我规定所有成功响应是{ code: 0, message: ok, data: ... }失败是业务错误码加message。前端拦截器按code处理。不要贪图方便把错误也返回200这样前端没法优雅处理。这个约定虽然简单但整个团队通信成本会低很多。6. 部署上线与性能优化建议6.1 前后端部署Node 服务与 Nginx 的分工开发完成后部署我建议分三步。第一步前端打包在client目录执行npm run build生成dist静态目录。第二步后端代码直接上传到服务器用pm2保持进程常驻pm2 start app.js --name server。第三步在服务器上配置Nginx网站根目录指向dist同时将/api请求反向代理到Node服务。下面是一个要点示例server { listen 80; server_name your.domain.com; root /var/www/dist; location / { try_files $uri $uri/ /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; } }这里注意try_files配置Vue是单页应用刷新非首页路由时如果没有这个配置会直接404。Nginx需要把请求都指到index.html。另外后端服务不要监听公网端口让Nginx代理访问安全性和稳定性都会好很多。6.2 选课高峰期的缓存与异步化思路选课高峰期可能会遇到两个性能瓶颈课程列表的查询和选课事务的并发。课程列表是读多写少的典型可以在Redis里缓存当前学期的课程列表管理员变更课程时同步清缓存。选课事务的并发则可以通过引入Redis预扣库存来优化但需要小心库存一致性问题复杂度不是第一版该背的。我的建议是第一版先保证事务正确性第二版再用缓存优化。缴费通知如果走真实支付渠道后端回调要及时响应不能长时间阻塞。支付回调的业务处理可以丢进任务队列异步执行比如用BullMQ或简单的SetImmediate先把200响应返回给支付平台再慢慢更新订单状态。这个细节对线上稳定性很重要。6.3 从项目实战总结的几条原则回顾这个项目有几条原则是实打实换来的。第一状态字段先定义清楚再写接口选课状态和缴费状态一开始就要列全不然后期改表代价很大。第二权限不要只靠前端隐藏按钮后端每个接口都要校验角色JWT中间件是最省事的方式。第三并发问题提前想不要让数据库帮你背锅。第四接口返回结构统一前后端配合效率会暴涨。最后再分享一个小经验给选课接口写一个简单并发脚本用node跑几十个并发请求打一下大部分并发问题都能在开发阶段暴露出来别等上线后才被学生薅出并发Bug。这套系统的核心数据流一旦在开发期验证过后面接真实支付、加缓存、上集群都会从容很多。
返回列表