ARTICLE DETAIL

资讯详情

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

基于Node.js和Vue的中医在线课程购买系统全栈开发详解

基于Node.js和Vue的中医在线课程购买系统全栈开发详解 做中医在线学习课程购买服务管理系统前后端选型时我几乎没有犹豫直接锁定了 Node.js Vue 这套组合。整套系统做下来从课程展示、下单购买到视频在线播放、后台管理核心功能全部落地今天把整个项目的设计思路、实操步骤和踩坑记录完整拆一遍想上手这类全栈项目的朋友可以直接参考。系统本身的定位很清晰面向中医爱好者、系统学习者和从业者提供课程浏览、课程购买、在线学习、个人中心以及后台管理一整套闭环服务。用户端要能看课、买课、学课管理端要能管课程、管订单、管用户这两条线正好对应到 Vue 前端和 Node.js 后端的职责划分前后端分离的思路从第一天就定下来。1. 项目整体设计与技术选型1.1 系统定位与功能拆解先交代清楚这个系统到底做了什么。整个平台按角色拆成三块普通用户、课程讲师或运营人员、系统管理员。用户侧的核心链路是“注册登录 → 浏览课程 → 购买下单 → 在线学习”管理侧的核心链路是“课程发布 → 订单管理 → 用户管理 → 数据统计”。围绕这个业务模型功能模块可以拆成这么几块用户模块注册、登录、个人信息维护、密码修改课程模块课程分类、课程列表、课程详情、视频章节展示订单模块课程下单、订单状态流转待支付/已支付/已取消、模拟支付回调学习模块已购课程列表、视频点播、学习进度记录后台管理模块课程上下架、课程分类管理、订单查询、用户禁用/启用这里有一个很容易忽略的设计点课程内容属于数字商品订单一旦完成用户获得的是观看权限而不是实物物流所以订单表和课程学习权限表需要做成联动支付成功后自动给用户开通对应课程的观看权限。这个逻辑在数据库设计阶段就要想清楚否则后面做权限校验会到处补丁。1.2 为什么选 Node.js Vue选型这件事团队内部其实是吵过一轮的。有人提议 Spring Boot Vue理由是 Java 在传统企业级项目里更“稳”。但我坚持用 Node.js原因很实际整个项目只有两个人开发Node.js 从接口开发到数据库操作是同一套语言体系前端组员也能快速接手后端代码沟通成本低非常多。Node.js 这边我用的是 Express 框架轻量、中间件生态成熟路由、参数解析、静态资源处理都有现成方案。Vue 这边选了 Vue 2项目启动时 Vue 3 还没完全普及配合 Element UI 做后台管理界面组件丰富开箱即用。前后端通过 RESTful API 通信数据格式统一用 JSON调试起来非常直观。对比一下 Spring Boot Vue 的传统方案Node.js 在两类场景有明显的优势一是快速原型验证一个简单的 CRUD 接口从路由到数据库可能只需要十几行代码二是前后端语言统一类型定义、字段命名可以共用一套习惯特别适合小团队协作。当然 Node.js 也有短板CPU 密集型任务处理能力一般但在线学习系统的主要负载在 I/O数据库读写、文件流和网络传输上Node.js 的事件驱动模型恰好擅长这类场景。提示选型不是越复杂越好要结合团队规模、业务类型、上线周期综合判断。如果团队全是 Java 背景盲目上 Node.js 反而是灾难。2. 环境搭建与踩坑记录2.1 Node.js 安装与 npm 配置这个项目的第一步就是装 Node.js。这里顺便说一句很多新手在“环境配置”这个环节就被劝退实际上只要搞懂了 Node.js 的安装机制和 npm 的全局配置后面会很顺。Node.js 安装建议直接去官网下载 LTS 版本Windows 下是一个 .msi 安装包双击一路 Next 就行。安装过程中需要注意一点安装路径尽量不要带空格和中文这个项目的机器上有人装到了D:\Program Files (x86)\nodejs后面 npm 全局包安装时经常出怪问题路径解析会有干扰。装完以后验证一下node -v npm -v能正常输出版本号说明 Node.js 本体装好了。接下来配置 npm 的全局目录和缓存目录默认情况下 npm 会把全局包装到系统盘权限和空间都是隐患npm config set prefix D:\nodejs\node_global npm config set cache D:\nodejs\node_cache再配置一下环境变量把D:\nodejs\node_global加进 PATH这样全局安装的 vue-cli、cnpm 之类的工具命令才能直接使用。2.2 高频报错npm.ps1 加载失败这个报错在热词里出现了好几次非常典型Windows 用户大概率会碰到npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。原因很简单Windows 默认的 PowerShell 执行策略是 Restricted禁止运行任何 .ps1 脚本文件而 npm 的全局命令其实是一个 .ps1 脚本PowerShell 直接把执行拦截了。解决办法有两个。第一个以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned这个策略的意思是本地创建的脚本可以运行从网络下载的脚本必须有数字签名。改完以后重新打开终端npm 命令就正常了。第二个办法更简单直接用 CMD 而不是 PowerShell.cmd 脚本不受执行策略限制。我个人的建议是不要为了一时方便去改成 Unrestricted那会让所有来源不明的脚本都能在机器上执行安全风险太高。RemoteSigned 是 Windows 平台的推荐配置既能满足正常开发需求又保留了安全边界。2.3 Vue 工程创建与依赖安装前端工程我用 vue-cli 创建vue create tcm-front按需选择 Vue Router、Vuex、Less 等选项。项目创建好后安装依赖时也有一堆坑。npm 默认源在海外国内网络环境下安装速度经常让人崩溃换成淘宝镜像是最有效的方案npm config set registry https://registry.npmmirror.com实测下来换源以后安装 Element UI、axios 这些核心依赖的速度能快好几倍。这里有一个细节锁文件要保留好package-lock.json 确定了团队所有成员安装到的依赖版本保持一致避免“我这能跑你那就报错”的尴尬。3. 数据库设计与后端 API 实现3.1 核心数据表设计数据库选了 MySQL原因很朴素业务是典型的强事务场景订单、支付、权限MySQL 的稳定性和事务支持足够可靠。表结构围绕业务链路设计核心表有这么几张表名核心字段作用usersid, username, password, real_name, role, status用户账号与角色course_categoryid, name, parent_id, sort, status课程分类courseid, category_id, title, cover, price, description, status课程基础信息course_chapterid, course_id, title, video_url, duration, sort课程章节与视频地址ordersid, order_no, user_id, course_id, amount, status, pay_time订单记录course_userid, user_id, course_id, create_time用户已购课程关系表study_progressid, user_id, chapter_id, progress, finish_time学习进度记录这里有几个设计经验值得单独说。订单号是业务上的硬要求绝对不能直接用自增 ID要生成一个唯一的业务单号我用的是“时间戳 用户ID 随机数”的组合格式类似202406011230150001前端展示、客服沟通、后续对账都靠这个单号定位。密码存储不要用明文更不要自己发明加密算法。我用 bcrypt 加盐哈希每个用户生成独立的盐值即使数据库泄露还原原始密码的代价也高得多。登录认证采用 JWTJSON Web Token无状态认证方式对前后端分离架构非常友好服务端不需要保存会话信息token 过期时间设为 24 小时客户端存储到 localStorage。course_user 表是实现“购买后解锁观看权限”的关键。用户在订单支付成功后后端在同一个数据库事务里完成两件事更新订单状态为已支付、向 course_user 表插入一条关联记录。这样用户查看课程详情时只要判断 course_user 里有没有对应记录就能决定是显示“立即购买”还是“开始学习”。3.2 Express 接口分层设计后端代码我在项目初期就定了分层规范避免所有逻辑堆在路由里变成一坨。每个业务模块按三层拆分路由层routes只负责定义 URL 和 HTTP 方法映射控制层controllers负责接收参数、调用服务、返回响应服务层services负责核心业务逻辑操作数据库拿课程购买功能举例路由文件长这样const express require(express); const router express.Router(); const orderController require(../controllers/orderController); const authMiddleware require(../middleware/auth); router.post(/create, authMiddleware, orderController.createOrder); router.post(/pay, authMiddleware, orderController.payOrder); router.get(/list, authMiddleware, orderController.getMyOrders); module.exports router;控制层和数据库操作层分开以后最大的好处是测试和维护成本低。排查问题的时候先看路由收到了什么请求再看控制器怎么处理最后才查数据库语句不会像单文件那样到处都是。接口设计上统一返回结构也是一个必须提的事。我定义了一套公共响应格式所有接口都长一个样{ code: 0, // 0表示成功非0表示失败 message: success, data: {} }前端 axios 封装里统一做响应拦截code 不为 0 时直接弹出错误提示业务代码不需要每个接口都写一遍错误分支。这个约定看起来简单但对开发效率的提升是立竿见影的。3.3 JWT 认证与权限校验说一个敏感但必须做的事情权限控制。这个系统有普通用户和管理员两种角色不可能让所有接口对所有人开放。JWT 的签发在登录接口完成。用户提交用户名密码后端验证通过后生成一个包含用户 ID 和角色的 token签名密钥放在服务端的 .env 环境变量里绝对不能硬编码到代码中。前端在 axios 请求拦截器里统一加上请求头axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });后端用一个 auth 中间件统一校验const jwt require(jsonwebtoken); module.exports function (req, res, next) { const token req.headers.authorization?.split( )[1]; if (!token) { return res.status(401).json({ code: 401, message: 未登录 }); } try { const decoded jwt.verify(token, process.env.JWT_SECRET); req.user decoded; next(); } catch (e) { return res.status(401).json({ code: 401, message: 登录已过期 }); } };需要管理员权限的接口再包一层 admin 中间件校验 decoded 里的角色字段。这套机制写起来不难但能让系统的安全性提高一个级别。4. 核心功能模块实现4.1 课程展示与购买流程课程列表页是最先做的前端页面调课程接口拿数据用卡片组件展示封面、标题、价格和简介。这里有一个体验细节价格字段在数据库里用整数分存储接口返回也是分前端展示时要除以 100 转换成元避免浮点运算精度问题。课程详情页的核心是权限判断。用户点进来前端先请求课程详情接口接口内部会根据当前用户是否已购买返回不同的数据已购用户返回完整课程信息包括视频章节列表未购用户返回课程基础信息视频地址字段为空这样设计不用前端猜状态后端一次接口就把权限规则定死了。购买流程走的是标准的“创建订单 → 发起支付 → 支付回调”三步。真实支付对接微信或支付宝都涉及商户号申请和回调验签流程我做的是模拟支付通道前端点击“立即支付”后端生成一个支付二维码页面轮询订单状态当模拟支付成功或者测试环境直接调回调接口订单状态更新为已支付同时开通课程权限。4.2 视频在线播放与 m3u8 格式在线学习系统的重头戏是视频播放这里要重点聊 m3u8。课程视频我采用的是 HLS 协议视频切片成一个个 .ts 分片文件用 .m3u8 索引文件来引导播放器依次加载。HLS 在移动端和 Web 端的兼容性都很好而且支持自适应码率弱网环境下会自动切换较低清晰度的分片不会直接卡死。前端播放器我选了 video.js配合 hls.js 插件在 Vue 组件里封装成一个视频播放组件。核心代码大概是这样template div classvideo-player video refvideoPlayer classvideo-js vjs-default-skin/video /div /template script import videojs from video.js; import video.js/dist/video-js.css; import videojs-contrib-hls; export default { name: VideoPlayer, props: { source: { type: String, required: true } }, mounted() { this.player videojs(this.$refs.videoPlayer, { sources: [{ src: this.source, type: application/x-mpegURL }], controls: true, autoplay: false, preload: auto }); }, beforeDestroy() { if (this.player) { this.player.dispose(); } } }; /script视频文件的存储和分发用的是 MinIO 对象存储。MinIO 是开源的可以本地私有化部署接口兼容亚马逊 S3 协议。课程视频上传到 MinIO 后后端对存储路径做一层权限访问控制学习接口返回的 m3u8 地址带有时效签名防止未购买用户直接通过 URL 盗看。有一点要特别注意m3u8 里引用的 .ts 分片地址如果是相对路径播放器拼接时会依赖 URL 的 base 地址如果走了 CDN 或者反向代理路径拼接规则不一致会导致播放失败。我在实际项目里的做法是后端返回视频地址时对 m3u8 文件内容做一次重写把分片地址统一替换成完整的带签名地址。4.3 学习进度记录学习进度这个功能不算复杂但很影响体验。用户在观看视频时前端每 15 秒上报一次当前播放位置后端写入 study_progress 表。用户再次打开课程时接口返回上次播放位置播放器初始化时通过currentTime直接跳转到对应秒数。这里有一个并发问题连续上报的时候如果每次都走完整的事务更新数据库压力会比较大。我做了个简单的优化前端在上报时带上章节 ID 和播放位置后端按“章节 ID 用户 ID”做 unique 索引直接执行 upsert存在即更新不存在则插入再配合节流实际对数据库的写入频率完全可以接受。进度数据还有一个上游价值就是管理后台的统计报表。通过汇总每个课程的平均完成率运营人员能直观看到哪些课程内容受欢迎哪些章节跳出率特别高这对后续课程内容优化非常有用。4.4 管理后台管理后台用 Vue Element UI 搭了一套和用户端独立的界面。这里要提一下动态路由的实践不同角色登录后看到的菜单和能访问的页面是不同的。管理员登录后拉取自身权限对应的路由表前端通过 Vue Router 的addRoutes动态注册而不是把所有的路由都写死在静态路由表里有效减少非授权页面的暴露面。经常有同学问 Vue 路由参数怎么用拿这个项目举个例子后台编辑课程页面 URL 是/admin/course/edit/:id路由定义参数名id页面里用this.$route.params.id读取然后调用courseService.getDetail(id)回填表单。编辑保存时判断有 id 就走更新接口没有就走新增接口一条路由同时服务两种场景。对于课程管理、订单管理这类数据列表Element UI 的 el-table 配合分页组件是标配。封装一个通用的分页混入mixin把 currentPage、pageSize、total 和加载函数逻辑统一管理每个列表页就不需要重复写分页代码了。5. 前端工程化与状态管理5.1 Vue Router 与路由守卫前端路由表我按模块划分使用懒加载让页面组件按需加载避免首屏 bundle 过大const routes [ { path: /, component: () import(../views/Home.vue) }, { path: /course/:id, component: () import(../views/CourseDetail.vue) }, { path: /learn/:courseId, component: () import(../views/Learn.vue), meta: { requiresAuth: true } }, { path: /admin, component: () import(../views/admin/Layout.vue), meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: course, component: () import(../views/admin/CourseManage.vue) }, { path: order, component: () import(../views/admin/OrderManage.vue) } ] } ];路由守卫承担了前端的访问控制。全局前置守卫里每次跳转前先检查meta.requiresAuth未登录带着 token 也先跳登录页保证前端层面就不会把未授权的页面泄露给用户。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });登录后需要调到原本想访问的页面就是靠redirect参数实现的这个细节体验很好不要让用户登录完还停留在首页自己去重新找。5.2 状态管理与页面数据流转Vuex 在这个项目里主要管三类全局数据用户信息、购物车/订单状态、全局通用配置。用户登录成功后接口返回的用户信息存入 Vuex同时同步一份到 localStorage刷新页面时重新 hydration避免用户刷新一下就看到“未登录”。共享状态还有一个典型的场景是不同组件之间的通信比如视频播放器和课程大纲侧边栏之间。用户在大纲里点击另一个章节需要让播放器组件切视频这时候如果组件层级很深props 一层层传太麻烦用 Vuex 的 action 触发一次状态变更播放器组件 watch 这个状态干净利落。5.3 Element UI 二次封装与表单校验后台管理页面高频使用的组件表格、弹窗、表单我做了二次封装减少重复代码量。最典型的是封装了一个 ProTable 组件把数据请求、分页、加载状态、空数据展示全部内置页面里只需要传入列配置和数据接口函数pro-table :columnscolumns :fetch-datafetchData :paramssearchParams /表单校验是容易被低估的模块。用户注册、管理员添加课程等场景都有复杂的校验规则Element UI 自带 async-validator但要记住内置校验规则满足不了所有场景。比如课程价格必须是大于 0 的数、视频地址必须是合法 URL这些都需要自定义 validator写的时候注意校验函数里要调用 callback否则表单永远校验不通过。6. 常见问题与排查实录6.1 跨域请求问题前后端分离部署之后跨域是必定会遇到的问题。开发环境下前端跑在 8080 端口后端跑在 3000 端口前端请求后端接口浏览器默认会拦截非同源的请求。我的处理方式是在 Vue CLI 里配置开发服务器代理// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } };前端请求/api/course/list开发服务器把请求转发到http://localhost:3000/api/course/list浏览器看的是同源跨域问题直接消失。生产环境则交给 Nginx 反向代理同一个域名下用 location 规则区分前端静态资源和后端 API也不存在跨域问题。6.2 Electron 打包为桌面应用项目后期有人提出想要桌面客户端我把 Vue 项目接入了 Electron。核心思路是主进程和渲染进程分工主进程负责创建窗口和系统底层操作渲染进程就是原有的 Vue 应用。封装 ipcMain 和 ipcRenderer 通信时要特别注意渲染进程不能直接访问 Node.js API安全模式下所有数据交换都要通过 IPC 通道。比如桌面应用里使用本地文件存储下载课程渲染进程发送请求到主进程主进程拿到文件路径后做读写操作再把结果返回。开发模式下我用 electron-builder 配置打包脚本一键生成 Windows 安装包。这个过程中遇到最多的问题是白屏。Electron 加载 Vue 应用时开发环境要指向 devServer 地址生产环境要指向打包后的 dist/index.html两个环境的 baseURL 不一致很容易白屏。解决方案是在打包脚本里根据环境变量设置不同加载路径并且把 Vue 路由的 history 模式改成 hash 模式避免静态资源路径解析问题。6.3 视频加载失败与权限过期排查视频播放器偶尔黑屏排查思路从两个方向入手先确认 m3u8 地址能不能直接访问用浏览器单独打开地址如果能播放问题出在播放器参数上如果不能播放看返回的是 403 还是 404。403 说明签名过期或 IP 限制404 则是文件路径不对。这类问题我最后用了一个排查技巧浏览器开发者工具看 Network 面板m3u8 请求返回后逐个查看 .ts 分片请求的状态码。有一次排查发现部分 .ts 请求 404原因是视频切片工具生成的索引文件里分片文件名带了原始目录前缀而 MinIO 存储路径里没有对应的子目录重新规划存储目录结构后解决。7. 部署经验与开发体会后端部署我用 PM2 做进程守护。Node.js 运行在单线程上一旦代码抛异常且没有捕获进程就会退出PM2 能在进程挂掉后自动重启并记录日志是我在生产环境最常用的工具。Nginx 负责反向代理和静态资源托管前端构建后的 dist 目录直接放在 Nginx 下。部署时的环境变量问题提醒一句.env文件里的数据库密码、JWT 密钥在生产环境一定要换成强密码和新密钥不要测试环境的密钥带到线上。密钥泄露 管理后台直接裸奔这个教训值得每一位开发者重视。最后说一点体会。前后端分离项目的复杂度不仅在于代码更在于对“状态流转”的把握一条数据从前端到后端再到数据库中间经过参数校验、权限校验、业务逻辑处理每一环都要有清晰的定义与容错方案。Express 在路由层要对参数做基础校验控制层处理业务逻辑服务层操作数据各司其职才不至于后期维护起来四处着火。做这个项目时我踩过的几个值得说的小技巧后端统一返回结构后前端封装拦截器统一处理提示订单支付成功的处理不要只更新订单表一定要在同一事务里开通课程权限所有上传的视频文件一律通过后端接口返回地址不要在前端写死存储路径。把这些细节做好了系统的可靠性和可维护性会明显上一个台阶。如果你正在做一个类似的在线教育项目这套基于 Node.js Vue 的架构思路可以直接抄作业。项目代码写完了不是终点迭代维护才是真正的开始把基础打好后续扩展直播课、电子书、题库这些业务模块都会顺畅很多。
返回列表