Node.js+Vue体育商城与场地预约系统全栈开发实战

发布时间:2026/9/29 3:29:12
Node.js+Vue体育商城与场地预约系统全栈开发实战
做这个 Node.js Vue 的体育用品商城与场地预约系统前后大概折腾了三个多星期。不夸张地说这项目虽然挂了两块业务但真正的难点不在“商城”也不在“预约”本身而是这两块业务混在一个系统里时订单状态怎么流转、场地时段怎么不冲突、权限怎么控制。前端技术栈是 Vue 2 Vue Router Vuex Element UI后端用 Node.js Express MySQL。项目代号 kdm1z整体走的是前后端分离路线适合用来练手也适合当毕设或项目经验去完整跑一遍。如果你是刚把 Node.js 装好、Vue 脚手架刚拉下来还没真正把一个前后端分离的项目打通那这篇文章应该能帮你少踩几个坑。我会把从环境搭建、数据库设计、后端接口、前端页面到联调排错的完整链路按我实际开发顺序原原本本拆一遍。1. 项目整体设计与模块拆解1.1 核心业务模块梳理这个系统从用户视角看是两块互有交集的功能一块是体育用品商城一块是场地预约。两者共用同一套用户体系所以在设计上不是“商城系统”和“预约系统”两个独立项目拼在一起而是共用用户表、共用鉴权、共用后台管理框架。商城的核心是商品、购物车、订单。场地预约的核心是场地、时段、预约单。这两块的共同点在“订单”上——商品订单和预约单本质上都是用户创建、需要状态流转的数据记录只不过商城的订单关联的是商品快照预约单关联的是场地时段。页面结构我按三端来拆游客端商品列表、商品详情、场地列表、场地详情、登录注册。用户端登录后购物车、提交订单、我的订单、场地预约、我的预约、个人中心。管理端商品管理、分类管理、场地管理、预约管理、订单管理、用户管理、数据统计。管理端和用户端在路由上做权限区分用 Vue Router 的守卫控制后端接口用 token 做身份校验。这样拆的好处是业务边界清晰后面写接口和调接口时不会左右打架。1.2 技术选型为什么是 Node.js Vue选 Vue 做前端基本不用纠结。Vue 的数据响应式对这类业务系统来说非常顺手组件化的思维适合把商品卡片、场地列表、订单状态标签这些高频复用的 UI 拆成独立组件。再加上 Element UI 直接提供表格、表单、日期选择器、弹窗这些组件后台管理页面的开发速度能快不少。选 Node.js 做后端核心原因是全栈技术栈统一。前端写 Vue 用 JavaScript后端用 Node.js 还是 JavaScript心智负担小。Express 框架轻量、生态成熟写 RESTful API 非常直接。这个项目的数据模型不算特别复杂不需要 Spring Boot 那种重框架的约束Express 的灵活度反而更合适。数据库用 MySQL因为它对这种关系型业务数据的支持最稳。商品、订单、用户、预约之间都有明确的外键关系用 MySQL 的联表查询处理起来最顺手。开发环境用 Navicat 或命令行工具建库建表都行生产环境建议至少做一次数据量测试后面我会讲到我碰到的数据量问题。1.3 数据库表结构设计表设计我拆成十个核心表这里重点说几个容易出问题的。用户表sys_userid、username、password、nickname、phone、avatar、role、create_time。role 字段区分管理员和普通用户密码用 MD5 加盐或 bcrypt 加密存储。商品表productid、name、description、price、stock、image、category_id、sales、status、create_time。这里有三个点要注意price 用 decimal(10,2)不要用 float避免精度问题stock 在每次下单时做减库存操作要配合事务使用category_id 关联分类表列表页按分类筛选就是用这个字段。购物车表cartid、user_id、product_id、quantity、checked、update_time。一个用户对同一个商品只保留一条记录重复加购就更新 quantity这个逻辑在接口里处理。商城订单表shop_orderid、order_no、user_id、total_price、status、create_time、pay_time、consignee、phone、address。status 用数字表示0 待付款、1 已付款待发货、2 已发货、3 已完成、4 已取消。场地表venueid、name、description、image、price_per_hour、open_time、close_time、status。场地类型比如篮球场、羽毛球场、乒乓球室等可以用单独的 type 字段或者再建分类表。预约表appointmentid、appointment_no、user_id、venue_id、book_date、start_time、end_time、status、create_time、total_price。status 这里要注意我用 0 待支付、1 已预约成功、2 已取消、3 已完成。book_date 是日期start_time 和 end_time 是时段比如“2024-06-10 18:00-20:00”。2. 开发环境搭建与 Node.js/Vue 工具链准备2.1 Node.js 安装与环境变量配置老生常谈但又是最容易出岔子的一环。我第一次装 Node.js 时一路默认安装结果后来在命令行里敲 node -v 能出来版本号但用 npm 全局装东西时总报各种奇怪的权限错误。问题就出在安装路径和环境变量上。建议安装时不要装在 C:\Program Files 这种带空格的路径下尽量装到 D:\nodejs 或者 C:\nodejs 这种纯英文无空格目录。安装完成后要确认两个环境变量PATH 中已有 D:\nodejs新增 NODE_PATH 环境变量值为全局 node_modules 路径。npm 全局模块的默认目录可以这样改把依赖装到自己的目录而不是系统目录npm config set prefix D:\nodejs\node_global npm config set cache D:\nodejs\node_cache这样做的目的是避免后面用 npm install -g 安装的全局工具比如 vue-cli、nodemon因为权限问题装不上也方便自己维护版本。装完以后给 PATH 里补上D:\nodejs\node_global重启终端后全局工具就能直接用了。2.2 Vue 项目创建与依赖安装前端项目我用 Vue CLI 创建指令很简单npm install -g vue/cli vue create sports-mall-front创建过程中会让你选预设我选的是 Manually select features然后把 Router、Vuex、CSS Pre-processors 勾上。这样脚手架会自动把 Vue Router 和 Vuex 集成好省得后面自己加。依赖安装阶段需要装这些核心包npm install element-ui axios vue-router3 vuex3注意我把 vue-router 和 vuex 锁在了 3.x 版本因为如果脚手架默认创建的是 Vue 2 项目对应的 Router 必须是 3.x、Vuex 必须是 3.x。如果你装到了 4.x 版本启动时大概率会直接报版本不匹配。这一点尤其容易忽略。2.3 npm 运行脚本被禁止问题开发期间几乎天天遇到的报错是npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这个问题的本质是 Windows PowerShell 的执行策略限制了 .ps1 脚本的运行。用管理员身份打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser之后再用 npm 就不会有这个限制了。这个报错在 VS Code 的终端里最常见因为 VS Code 默认终端就是 PowerShell。后面如果再遇到确认一下当前用户作用域的执行策略即可。2.4 项目初始化的目录结构开发之前我先规划好了前后端的目录结构。前端和后端放在不同的文件夹里避免混在一起sports-mall/ ├── frontend/ # Vue 前端项目 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # Vuex 状态管理 │ │ ├── views/ # 页面组件 │ │ ├── utils/ # 工具函数 │ │ ├── App.vue │ │ └── main.js ├── backend/ # Node.js 后端项目 │ ├── routes/ # 路由定义 │ ├── controllers/ # 控制器层 │ ├── models/ # 数据库模型 │ ├── middlewares/ # 中间件鉴权、日志 │ ├── utils/ # 工具函数 │ ├── app.js # 入口文件 │ └── db.js # 数据库连接这个结构的好处是后端路由和控制器分离前端 api 封装统一管理所有请求后面联调时排查问题非常快。3. 前端 Vue 核心功能实现3.1 商城模块商品列表、购物车与订单流程商城这块前端页面不算难难点在数据状态的管理。商品列表页我用的是卡片式布局每个卡片显示商品图、名称、价格、销量和“加入购物车”按钮。列表的数据是通过 axios 请求后端接口拿到的首次加载时展示全部商品选择分类后按 category_id 重新请求。价格显示这里有一个前端小坑后端返回的价格是199.00这样的 decimal 字符串如果不做处理直接显示会觉得很突兀。我在 utils 下写了一个 formatPrice 函数统一格式化成两位小数并保留货币符号export function formatPrice(price) { return ¥ Number(price).toFixed(2); }购物车页面我用的 Vuex 管理购物车数据。购物车的增删改查、选中状态、总价计算都放在 store 里。总价的计算用 getter 来实现视图层只需要读取不用在组件里写一堆重复计算逻辑getters: { selectedItems: state state.cart.filter(item item.checked), totalPrice: (state, getters) { return getters.selectedItems.reduce((sum, item) { return sum item.price * item.quantity; }, 0); } }订单提交时前端把选中的购物车商品列表、收货信息、总价一起 POST 到后端接口。后端生成订单号、扣减库存、清空购物车这些操作在一个数据库事务里完成。3.2 场地预约模块日期与时段的选择逻辑场地预约的前端比商城复杂在交互上。用户查看场地详情页要选择“哪一天”和“哪个时间段”然后才能提交预约。日期部分我直接用 Element UI 的el-date-picker限制只能选今天之后的日期。时段部分我预置了每个场地的开放时间段比如“08:00-10:00、10:00-12:00、14:00-16:00、16:00-18:00、19:00-21:00”这些固定档位。关键点是用户选了日期后要立刻请求后端接口查询这一天这个场地哪些时段已经被人预约了把被占用的时段置灰禁用。我封装了一个接口export function getBookedSlots(venueId, date) { return request({ url: /appointment/bookedSlots, method: get, params: { venueId, date } }); }返回的数据是[08:00-10:00, 19:00-21:00]这种已预约时段数组。前端拿到后跟预置的所有时段做一个过滤用 computed 计算属性生成可用时段列表禁用不可选的时段。对应到 Element UI 的el-radio-group或自定义按钮组都可以重点是禁用逻辑要写在数据层而不是组件里。3.3 路由配置与登录守卫路由配置这步非常关键。我用 Vue Router 做前端路由同时在路由守卫里做登录跳转。路由分了几块/login、/register、/首页、/product/:id商品详情、/cart购物车、/venue场地列表、/venue/:id场地详情、/order/list我的订单、/appointment/list我的预约。管理端单独一块前缀是/admin包含商品管理、场地管理、预约管理、订单管理等页面。守卫逻辑用全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path.startsWith(/admin) !token) { next(/login); } else if (to.path /login token) { next(/); } else { next(); } });管理端路由我还在 meta 里加了一个requiresAdmin字段通过守卫再次校验当前用户的 role 是否为管理员防止普通用户直接通过改 URL 进入管理页面。虽然真正的安全性还是要靠后端接口的权限判断但前端层面做到“看不到进不去”能挡掉绝大多数随手操作。3.4 Axios 统一封装与拦截器前端所有接口请求我统一封装在一个 request.js 里。封装的核心是请求拦截器和响应拦截器。请求拦截器负责在每次请求的 header 里加上 token这样后端可以识别当前用户service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; });响应拦截器负责统一处理错误状态码和后端返回的业务 code。后端返回结构约定为{ code: 200, data: ..., message: ok }如果 code 是 401token 失效前端就清除 token 并跳转登录页如果是其他错误直接用 ElMessage 弹出后端返回的错误信息。这样做的好处是业务组件里不需要写一大堆 try-catch 和错误提示接口请求只是简单地const res await getUserInfo(); this.userInfo res.data;4. 后端 Node.js 接口设计与业务实现4.1 Express 项目结构与中间件设计后端入口在 app.js主要做初始化连接数据库、配置中间件、挂载路由。const express require(express); const cors require(cors); const bodyParser require(body-parser); const app express(); // 跨域配置 app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // 路由挂载 app.use(/api/user, require(./routes/user)); app.use(/api/product, require(./routes/product)); app.use(/api/cart, require(./routes/cart)); app.use(/api/order, require(./routes/order)); app.use(/api/venue, require(./routes/venue)); app.use(/api/appointment, require(./routes/appointment)); app.listen(3000, () { console.log(Server running on port 3000); });跨域用 cors 中间件解决这也是最省事的方案。因为前端开发时跑在 8080 端口后端跑在 3000 端口它们之间的跨域请求必须处理。cors 中间件允许所有来源访问开发环境完全够用。如果后面要上线可以再收紧为白名单模式。数据库连接信息单独放在 db.js 里const mysql require(mysql2); const pool mysql.createPool({ host: localhost, user: root, password: 123456, database: sports_mall_db, waitForConnections: true, connectionLimit: 10 });用 mysql2 的 promise 版本配合 async/await 写接口非常顺手。不过需注意连接池的 connectionLimit 不要设太大这个环境默认 10 就够设太大会白白占用数据库连接资源。4.2 预约冲突检测核心业务逻辑场地预约最核心的逻辑在后端就是判断用户选择的时段有没有跟已有预约重叠。先看预约表里已有的数据。比如一个用户要预约“2024-06-10 19:00-21:00”的篮球场。后端要做的就是在appointment表里查一下有没有同一日期、同一场地、状态为“已支付”且时段重叠的记录。时段重叠的判断条件总结下来就是四种情况新预约的开始时间在已有预约的区间内新预约的结束时间在已有预约的区间内新预约的区间完全包住了已有预约的区间新预约的区间完全包含在已有预约区间内。对应 SQL 条件可以写成SELECT id FROM appointment WHERE venue_id ? AND book_date ? AND status 1 AND start_time ? -- 新结束时间 AND end_time ? -- 新开始时间这里start_time newEndTime AND end_time newStartTime这一个条件就覆盖了上面四种重叠情况是判断区间重叠的标准写法。字段类型是 TIME 或者 DATETIME 都能比较因为 MySQL 会自动做时间比较。我把这个逻辑放在预约接口里事务中先查冲突有冲突就直接返回“该时段已被预约”没冲突才插入记录、生成预约单号。4.3 商城接口与库存扣减商城的核心接口包括商品列表、商品详情、加入购物车、查看购物车、提交订单、支付订单、取消订单、确认收货。提交订单是其中逻辑最重的接口。流程是前端把购物车选中的商品 ID 和数量传过来后端根据商品 ID 查出最新的价格和库存校验库存是否充足计算总价生成订单号并插入订单表扣减库存删除对应用户购物车记录全部操作在一个事务里。扣减库存时要用乐观锁防止高并发下超卖UPDATE product SET stock stock - ? WHERE id ? AND stock ?affectedRows 如果为 0说明库存不足事务回滚返回“库存不足”。4.4 接口鉴权从登录到权限校验登录接口在用户表里查账号密码密码我用了 md5 加密存储。登录成功返回一个 token——我用的是 jsonwebtoken 包生成 JWT里面包含 userId 和 role 信息。const jwt require(jsonwebtoken); const token jwt.sign( { userId: user.id, role: user.role }, secret_key, { expiresIn: 7d } );后端在需要登录的接口上挂一个auth中间件function auth(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, secret_key); req.userId decoded.userId; req.role decoded.role; next(); } catch (e) { return res.status(401).json({ code: 401, message: 登录已过期 }); } }需要管理员权限的接口再包一层adminAuth校验req.role是否为 1。这样用户查自己的订单时只能查WHERE user_id req.userId不会互相串数据。5. 前后端联调与调试排错实录5.1 跨域配置与前端代理前端开发服务器在 8080 端口后端在 3000 端口浏览器直接访问肯定跨域。我用 cors 中间件在后端解决了。不过如果你的后端不想做跨域处理也可以在前端开发时用 Vue CLI 的代理功能在 vue.config.js 里配置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } };两种方案选一个就行。我实际两个都用过开发时用 proxy避免后端配置风险部署时在后端开启 cors。这里提醒一点前端用 proxy 时请求的 URL 要写成相对路径/api/...不要写http://localhost:3000/api/...否则代理不会生效。5.2 Vue DevTools 与接口调试调试前端状态我强烈建议装一下 Vue DevTools 插件。在 Vue 2 项目里通过 DevTools 可以直接看到组件树、检查 data、状态管理器里的数据、current route 等排查“明明数据变了但页面没变”这种问题能省不少时间。接口调试我直接用浏览器开发者工具里的 Network 面板观察每个请求的状态码、请求体、响应体。常见问题就是 404路由挂了、500后端报错、401没带 token看 Network 能第一时间锁定方向。更进阶一点可以装 Postman 或 Apifox不过我更喜欢在浏览器里直接看实测返回。5.3 常见联调问题联调阶段最容易出的几类问题后端返回的字段名和前端不一致。比如后端返回create_time前端代码却用createTime导致页面显示不出来。这种问题最好约定后端统一返回驼峰命名或者前端用工具函数做字段名映射。时间格式不统一。日期字段后端返回的是字符串2024-06-10T10:00:00.000Z前端直接展示会很丑要格式化。数字精度丢失。价格和数量的计算如果直接用浮点数运算容易有精度问题我后端算总价用小数精确计算前端只负责展示。5.4 Payment/订单状态实测记录在实际测试过程中我最常跑的链路是“注册 → 登录 → 挑选商品 → 加入购物车 → 提交订单 → 支付 → 查看订单”。这条链路跑顺了商城模块基本没问题。场地预约的链路是“注册 → 登录 → 选择场地 → 选择日期时段 → 提交预约 → 支付 → 查看预约”。测试时有一个细节值得反复验证预约一个已占用的时段必须提示“该时段已被预约”。我用两个账号测试A 账号预约了2024-06-10 19:00-21:00的 A 场地B 账号再用同一个时段去预约时接口要返回冲突提示前端按钮置灰。这个逻辑值得多跑几遍。6. 常见问题排查与避坑指南6.1 高频问题速查表开发期间我收集了一些高频问题整理在这里基本覆盖了大部分 Node.js Vue 项目会遇到的坑。故障现象原因分析解决思路npm 无法加载 npm.ps1PowerShell 执行策略限制执行Set-ExecutionPolicy -Scope CurrentUser RemoteSignedvue 命令找不到全局 CLI 未安装或 PATH 未配置重装 vue/cli确认全局 bin 在 PATH 中8080 端口被占用Vue CLI 默认端口冲突修改 devServer.port或杀掉占用进程后端接口返回 404路由路径不匹配检查 app.js 中挂载的路由前缀和 routes 中的路径接口返回 500后端代码异常或 SQL 报错看后端控制台输出和堆栈日志跨域被拦截前后端端口不同未处理后端用 cors 或前端配 proxy中文乱码数据库连接字符集不对连接字符串加 charsetutf8mb4请求带不上 token请求拦截器未生效检查 axios 封装文件是否正确引入Vuex 数据刷新丢失state 默认存在内存中需要持久化用 localStorage 或插件路由跳转后页面空白组件路径写错或 import 漏了检查 router 配置和 console 报错6.2 后端接口 500 错误的排查思路后端 500 错误我是这样一条条排的第一步看终端控制台有没有打印出异常堆栈。Express 的错误处理中间件如果不加后端报错会被默认兜住但控制台还是能看到 error 信息。第二步确认 SQL 语句本身有没有问题。很多 500 是无心插柳的字段名打错导致的 SQL 报错。我习惯在开发环境把执行的 SQL 语句打印出来用 console.log 输出或者用 mysql2 的 debug 模式。第三步检查参数类型。前端传过来的是字符串18:00但后端表里字段是 TIME 类型MySQL 在某些模式下可能不会自动转换。JOIN 联表时要注意关联字段的类型是否一致。6.3 Vue 响应式丢失问题做商城购物车时我遇到过一个经典问题从接口拿到了购物车列表赋值给 data 里的cartList后页面上的数量修改不更新。后来发现问题出在接口一次返回了多层嵌套对象而 Vue 2 的响应式系统只能监听对象属性的新增和删除对于新增的深层属性无法触发视图更新。解决方式是在请求成功后重新赋值整个数组或者用this.$set(this.cartList, index, newItem)。更好的做法是在数据处理阶段就把需要的字段一次性铺平不要在视图层做深层嵌套操作。当时我直接在初始化时就写了完整的数据结构占位然后在赋值时整体替换就稳定了。6.4 预约时段显示与实际不符这个问题排查了很久才想明白。前端页面显示 19:00–21:00 这个时段后端查出来的start_time也是正确的但提交预约后列表页显示的时间比实际早了 8 小时。原因是数据库连接字符串没设置时区。MySQL 默认用的是系统时区Node.js 侧用的可能是 UTC导致写入和读取出现了时区换算差异。解决方式是连接串里明确加host: localhost, timezone: 08:00这行配置加上以后时间显示就正常了。这个坑在开发环境中尤其隐蔽因为白天测试不容易注意到时间差异但要处理不好用户预约时间会整体偏移。7. 实操心得与扩展建议7.1 开发顺序的建议这是我实际踩过一轮之后总结出来的顺序先做数据库表设计再做后端接口最后做前端页面。不要从页面倒推接口否则后面接口设计会非常被动。后端接口我建议按照“用户 → 商品 → 购物车 → 商城订单 → 场地 → 预约单”的顺序来写因为预约依赖场地订单依赖商品和用户底层先行会让后续工作顺畅很多。前端页面按“登录注册 → 商品列表 → 商品详情 → 购物车 → 订单 → 场地列表 → 场地详情 → 预约”的顺序来写每个页面完成后立刻测试对应接口问题不要攒着到最后一起排查。7.2 技术方案可扩展的方向这个系统跑通后还有几个方向可以继续深化。第一个是支付模块。现在我只做了模拟支付即点击“去支付”直接更新订单状态。如果要接入真实支付需要对接微信/支付宝支付接口涉及签名、回调验签、订单状态回查等多轮交互前后端都要加不少逻辑。第二个是通知模块。预约成功后可以给用户发短信或站内信提醒商城发货后也可以通知用户。可以用 Node.js 的 nodemailer 发邮件或者简单的站内消息表来实现。第三个是秒杀或限时优惠。商城系统加上限时折扣倒计时对并发能力会有更高要求这时候可以进一步研究 Redis 缓存、消息队列这些技术栈。写在最后从环境搭建到完整跑通这个项目我最深的体会有两点一是前后端分离项目的核心不是某个页面有多漂亮而是接口约定和数据流转是否清晰二是场地预约的时段冲突检测看着简单但如果不把start_time newEnd AND end_time newStart这个重叠判断公式吃透线上一定会出问题。开发中遇到的大部分问题归根结底都能在“环境配置”、“字段名约定”、“时间时区”和“事务边界”这四个维度里找到根因。把这几类问题在开发前就定好规范后端的坑能少一半。这个项目既是商城也是预约系统用 Node.js 和 Vue 做出来的体量足够支撑一次完整的全栈项目复盘。你在跑这个项目时如果也被某个时段冲突、某个 npm 报错卡住回过头来看看这篇文章应该能找到对应的解法。