ARTICLE DETAIL

资讯详情

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

Node.js+Vue+MySQL旅游系统实战:从环境搭建到订单状态机

Node.js+Vue+MySQL旅游系统实战:从环境搭建到订单状态机 简介这是一套基于Node.jsVueMySQL开发的高分毕业设计级旅游服务管理系统面向计算机专业本科生、课程设计与毕设学生解决旅游服务类Web系统从开发到部署的全流程实践需求。资源包共300个文件含82个Vue组件文件实现响应式前端界面、76个JS逻辑脚本支撑Node.js后端与前端交互、46个PNG/35个JPG图片资源含UI图标与页面素材、19个CSS样式文件及1个SQL数据库脚本含完整表结构与初始化数据整体压缩包61.13MB采用RAR格式封装。已有67人学习下载资源开箱即用包含VSCode项目工程、Navicat数据库配置说明、install.bat与run.bat一键启停脚本以及Babel、Webpack等构建配置文件目录结构规范模块职责清晰便于二次开发与功能拓展。1. 为什么“飞鸽旅游服务管理系统”要用 Node.js Vue MySQL 这套组合落地你正在接手一个高校课程设计或企业实训类旅游服务系统项目需求明确要管理景区信息、用户预订、订单状态、后台审核流程还要支持多角色游客、导游、管理员操作。这时候如果选 Java Spring Boot Thymeleaf开发周期长、前后端耦合重若用纯 PHP MySQL前端交互体验弱、难以支撑动态筛选和实时状态更新。而“飞鸽旅游服务管理系统”这个高分项目之所以被反复推荐核心在于它用Node.js 做轻量 API 层、Vue 做响应式前端、MySQL 做结构化数据底座三者形成低学习门槛、高交付效率、易扩展维护的闭环——尤其适合 24 人小组在 36 周内完成可演示、可部署、有业务逻辑深度的全栈系统。这套技术栈不是为炫技而是解决真实约束Node.js 的非阻塞 I/O 天然适配旅游系统中高频但低计算密度的请求如景点查询、余票刷新、订单提交Vue 的组件化让“首页轮播分类导航订单卡片后台表格”这类界面模块能快速复用MySQL 则提供事务保障比如支付成功后库存扣减与订单生成必须原子性且对地理信息字段如景区经纬度、区域编码支持稳定。如果你正卡在“怎么把旅游业务逻辑落到代码里”而不是纠结“该不该用微服务”那这个组合就是当前阶段最务实、最易出分、最能体现工程能力的选择。2. 搭建 Node.js Vue MySQL 开发环境绕过 npm.ps1 权限报错与依赖冲突2.1 解决 Windows 下npm : 无法加载文件 ... npm.ps1的根本方法很多同学在安装完 Node.js 后执行npm -v或vue create project时直接报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这不是 Node.js 安装失败而是 PowerShell 默认执行策略ExecutionPolicy阻止了本地脚本运行。不要改注册表不要用管理员身份运行终端——这些方案治标不治本且后续 CI/CD 环境会再次失效。正确做法是在普通用户权限的 PowerShell 中执行以下命令注意必须是 PowerShell不是 CMD 或 Git BashSet-ExecutionPolicy RemoteSigned -Scope CurrentUser执行后输入Y确认。该命令仅对当前用户生效允许本地签名脚本运行同时保留远程脚本的安全限制。验证是否生效Get-ExecutionPolicy -Scope CurrentUser # 输出应为 RemoteSigned提示此设置不影响其他用户也不降低系统安全性。若团队协作需将该命令写入项目 README.md 的「环境准备」章节避免新人重复踩坑。2.2 Vue CLI 5.x 与 Node.js 版本兼容性实测配置“飞鸽旅游服务管理系统”涉及地图组件如景区定位、富文本编辑如游记发布、表单校验如身份证号、手机号等典型功能对 Vue 版本有隐性要求Vue CLI 版本推荐 Node.js 版本关键适配点5.0.8v16.14.0 – v18.17.0支持script setup语法Vite 插件生态成熟vue-router4和pinia2兼容稳定4.5.15v14.15.0 – v16.13.0若需兼容 IE11极少数政务类旅游平台要求必须降级至此实际操作中我们采用nvm-windows管理多版本 Node.js避免全局污染# 下载 nvm-windows 安装包官网 github.com/coreybutler/nvm-windows # 安装后重启终端执行 nvm install 18.17.0 nvm use 18.17.0 node -v # 验证输出 v18.17.0 npm install -g vue/cli5.0.8注意vue/cli5.0.8是目前 Vue 3 生态中与element-plus旅游系统常用 UI 库和axiosAPI 调用配合最稳定的版本。高于 5.0.8 的版本在vue-router导航守卫与piniastore 初始化顺序上偶发竞态问题导致登录后路由跳转失效——这正是“飞鸽系统”中用户登录后白屏的常见根因。2.3 MySQL 8.0 安装与旅游业务库初始化脚本旅游系统对数据库有明确字段语义要求景区表需含lng经度、lat纬度、area_code行政区划码订单表需status0待支付,1已支付,2已出行,3已完成,4已取消用户表需role1游客,2导游,3管理员。不能直接用CREATE DATABASE flypigeon;了事。创建flypigeon_init.sql文件内容如下含中文注释与索引优化-- 创建数据库并指定 utf8mb4 字符集支持 emoji 及生僻字如景区名“龘” CREATE DATABASE IF NOT EXISTS flypigeon CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE flypigeon; -- 景区表重点建立 (area_code, status) 复合索引加速区域状态联合查询 CREATE TABLE scenic_spots ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景区名称, area_code CHAR(6) NOT NULL COMMENT 国标行政区划码如110101, lng DECIMAL(10,8) NOT NULL COMMENT 经度, lat DECIMAL(10,8) NOT NULL COMMENT 纬度, status TINYINT DEFAULT 1 COMMENT 状态1启用,0停用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_area_status (area_code, status) ) ENGINEInnoDB COMMENT景区信息表; -- 订单表status 字段必须加索引因后台常按状态筛选如查所有“已出行”订单 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, spot_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付,1已支付,2已出行,3已完成,4已取消, amount DECIMAL(10,2) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status (status), INDEX idx_user_spot (user_id, spot_id) ) ENGINEInnoDB COMMENT订单主表;执行导入mysql -u root -p flypigeon_init.sql提示MySQL 8.0 默认认证插件为caching_sha2_password若 Node.js 连接时报ER_NOT_SUPPORTED_AUTH_MODE需在 MySQL 中执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password;此命令将认证方式降级为兼容旧驱动的模式不影响安全性。3. 实现飞鸽系统核心业务从景点搜索到订单状态机的全链路代码3.1 Node.js 后端用 Express Sequelize 构建 RESTful API“飞鸽旅游服务管理系统”的 API 设计遵循资源导向原则URL 语义清晰GET /api/scenic-spots?area_code310115status1→ 查询浦东新区启用景区POST /api/orders→ 提交新订单含事务控制PUT /api/orders/:id/status→ 更新订单状态如管理员确认出行使用Sequelize替代原生 SQL既保证类型安全又避免 SQL 注入。定义ScenicSpot模型models/scenicSpot.jsmodule.exports (sequelize, DataTypes) { const ScenicSpot sequelize.define(ScenicSpot, { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING(100), allowNull: false, validate: { notEmpty: true } }, area_code: { type: DataTypes.STRING(6), allowNull: false, field: area_code }, lng: { type: DataTypes.DECIMAL(10, 8), allowNull: false }, lat: { type: DataTypes.DECIMAL(10, 8), allowNull: false }, status: { type: DataTypes.TINYINT, defaultValue: 1, validate: { min: 0, max: 1 } } }, { tableName: scenic_spots, timestamps: true, underscored: true // 自动将 createdAt 转为 created_at }); return ScenicSpot; };关键控制器controllers/scenicSpotController.js实现带条件的分页查询const { ScenicSpot } require(../models); exports.getSpots async (req, res) { const { area_code, status, page 1, limit 10 } req.query; const offset (page - 1) * limit; try { const { count, rows } await ScenicSpot.findAndCountAll({ where: { ...(area_code { area_code }), ...(status ! undefined { status: parseInt(status) }) }, limit: parseInt(limit), offset: offset, order: [[created_at, DESC]] // 新上线景区优先展示 }); res.json({ code: 0, data: { list: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit) } } }); } catch (error) { console.error(查询景区失败:, error); res.status(500).json({ code: 500, msg: 服务器内部错误 }); } };参数说明findAndCountAll一次性返回数据列表与总条数避免COUNT(*)SELECT两次查询order: [[created_at, DESC]]确保新录入景区在列表顶部符合旅游平台运营逻辑...(area_code { area_code })是 ES6 逻辑短路写法自动过滤空参数防止WHERE area_code NULL错误。3.2 Vue 前端用 Composition API 实现景点搜索与地图联动在src/views/HomePage.vue中用户输入区域码如“310115”后触发搜索并渲染地图标记。使用vueuse/core的useAsyncState管理异步状态更简洁template div classsearch-container el-input v-modelareaCode placeholder请输入行政区划码如310115 / el-button clickfetchSpots搜索/el-button /div !-- 使用高德地图 JS API 渲染标记 -- div idmap-container styleheight: 500px;/div el-table :dataspots stylemargin-top: 20px; el-table-column propname label景区名称 / el-table-column proparea_code label区域码 / /el-table /template script setup import { ref, onMounted } from vue; import { useAsyncState } from vueuse/core; import { getScenicSpots } from /api/scenicSpot; const areaCode ref(310115); const spots ref([]); // 初始化地图高德 API Key 需替换为实际申请的 onMounted(() { const map new AMap.Map(map-container, { zoom: 11, center: [121.5, 31.2] // 上海中心点 }); }); const fetchSpots async () { const { state, isReady } useAsyncState( () getScenicSpots({ area_code: areaCode.value }), [] ); if (isReady.value) { spots.value state.value.data.list || []; // 同步更新地图标记 updateMapMarkers(state.value.data.list); } }; const updateMapMarkers (spotList) { // 清除原有标记 AMap.event.trigger(map, clearOverlays); // 添加新标记 spotList.forEach(spot { new AMap.Marker({ position: [spot.lng, spot.lat], map: map, title: spot.name }); }); }; /script注意useAsyncState将请求逻辑与组件状态解耦state自动响应数据变化updateMapMarkers函数确保地图标记与表格数据严格同步避免“表格显示景区A地图却标在景区B”的一致性 bug——这是旅游系统演示时最易被评委质疑的细节。3.3 订单状态机用 MySQL 事务保证“支付成功→库存扣减→通知发送”原子性旅游订单的核心风险在于用户支付成功但景区余票未扣减导致超卖。必须用数据库事务兜底。在controllers/orderController.js中实现const { Order, ScenicSpot } require(../models); exports.createOrder async (req, res) { const { user_id, spot_id, amount } req.body; // 启动事务 const t await sequelize.transaction(); try { // 1. 查询景区当前状态必须是启用状态 const spot await ScenicSpot.findByPk(spot_id, { transaction: t }); if (!spot || spot.status ! 1) { throw new Error(景区不可用); } // 2. 创建订单status1已支付 const order await Order.create({ user_id, spot_id, status: 1, amount }, { transaction: t }); // 3. 【关键】此处应调用库存服务但飞鸽系统简化为景区表无库存字段故用“订单数”反向约束 // 实际项目中此处应更新 spot_stock 表的 remaining 字段并检查是否 1 // 为演示事务完整性我们模拟一个必须成功的操作 await ScenicSpot.update({ updated_at: new Date() }, { where: { id: spot_id }, transaction: t }); await t.commit(); // 提交事务 res.json({ code: 0, data: { order_id: order.id } }); } catch (error) { await t.rollback(); // 回滚事务 res.status(400).json({ code: 400, msg: error.message || 创建订单失败 }); } };提示真实旅游系统需独立库存服务但“飞鸽”作为教学项目通过事务强制绑定景区状态更新与订单创建已能体现 ACID 原则。若需扩展可在ScenicSpot表增加stock字段并在update语句中加入stock 0条件再用SELEC... FOR UPDATE加行锁——这正是 MySQL 处理高并发订票的工业级方案。4. 高分项目必备Vue 打包优化、MySQL 索引调优与跨域调试技巧4.1 解决 Vue 打包后布局异常的 3 个硬核检查点“飞鸽旅游服务管理系统”演示时出现“首页轮播图错位”“后台表格列宽压缩”等布局异常90% 源于打包配置疏漏。逐项排查问题现象根本原因修复方式Element Plus 组件样式丢失vue-cli-service build未启用 CSS 提取在vue.config.js中添加configureWebpack: { optimization: { splitChunks: { chunks: all } } }地图容器高度塌陷#map-container的 height 在 CSS 中被!important覆盖删除所有!important改用scoped样式或 CSS Modules路由懒加载后图标字体不显示public/fonts未被 Webpack 正确识别在vue.config.js中配置chainWebpack: config { config.plugin(html).tap(args { args[0].favicon ./public/favicon.ico; return args; }); }最关键的修复是静态资源路径配置。在vue.config.js中必须显式声明module.exports { publicPath: process.env.NODE_ENV production ? /flypigeon/ : /, outputDir: dist, assetsDir: static, // 重要确保 public 目录下 index.html 引用的 JS/CSS 路径正确 configureWebpack: { resolve: { alias: { : path.resolve(__dirname, src) } } } };注意publicPath设为/flypigeon/是为了支持 Nginx 部署在子路径如https://example.com/flypigeon/避免404 Not Found。若部署在根路径改为/即可。4.2 MySQL 查询性能诊断用 EXPLAIN 分析景区搜索慢的原因当GET /api/scenic-spots?area_code310115响应超过 1s需用EXPLAIN定位瓶颈EXPLAIN SELECT * FROM scenic_spots WHERE area_code 310115 AND status 1 ORDER BY created_at DESC LIMIT 10;重点关注三列列名正常值异常表现优化动作typeref或rangeALL全表扫描确认idx_area_status索引存在且被使用keyidx_area_statusNULL执行ANALYZE TABLE scenic_spots;更新统计信息rows 1000 10000检查area_code是否有大量重复值如全国景区都填“000000”需业务层校验若EXPLAIN显示key为NULL强制使用索引SELECT * FROM scenic_spots FORCE INDEX (idx_area_status) WHERE area_code 310115 AND status 1 ORDER BY created_at DESC LIMIT 10;提示FORCE INDEX仅用于临时诊断生产环境应通过ANALYZE TABLE和合理数据分布解决而非硬编码索引名。4.3 跨域调试Vue 开发服务器代理真实 API 的最小配置前端http://localhost:8080访问后端http://localhost:3000/api时浏览器报CORS错误。不要在 Node.js 后端加cors中间件应付了事——这掩盖了真实部署时 Nginx 反向代理的配置逻辑。正确做法是在vue.config.js中配置代理使开发期请求透传module.exports { devServer: { proxy: { /api: { target: http://localhost:3000, // 后端地址 changeOrigin: true, // 修改 Origin 头避免后端拒绝 pathRewrite: { ^/api: /api // 保持路径前缀不变 } } } } };此时前端代码仍调用axios.get(/api/scenic-spots)开发服务器自动转发至http://localhost:3000/api/scenic-spots且Origin头被设为http://localhost:8080完全模拟生产环境 Nginx 的proxy_pass行为。注意changeOrigin: true是必须项否则 Express 的req.headers.origin为null导致cors中间件判断失败。此配置让前端开发者专注业务无需操心后端跨域策略细节。5. 高分交付技巧用 MySQL 存储过程批量生成测试数据 Vue 动态路由权限控制5.1 用存储过程快速填充 1000 条景区测试数据评委演示时要求“随机查看多个区域景区”手动 INSERT 效率低下。编写 MySQL 存储过程generate_test_spotsDELIMITER $$ CREATE PROCEDURE generate_test_spots() BEGIN DECLARE i INT DEFAULT 1; DECLARE area_codes CHAR(6); DECLARE area_list VARCHAR(100) DEFAULT 110101,310115,440304,510104,210102; WHILE i 1000 DO -- 随机选取一个区域码 SET area_codes SUBSTRING_INDEX(SUBSTRING_INDEX(area_list, ,, CEIL(RAND() * 5)), ,, -1); INSERT INTO scenic_spots (name, area_code, lng, lat, status) VALUES ( CONCAT(飞鸽景区-, i), area_codes, ROUND(120 RAND() * 20, 8), -- 经度范围 120~140 ROUND(22 RAND() * 10, 8), -- 纬度范围 22~32 FLOOR(RAND() * 2) -- status 0 或 1 ); SET i i 1; END WHILE; END$$ DELIMITER ; -- 调用 CALL generate_test_spots();执行后SELECT COUNT(*) FROM scenic_spots;返回1000且area_code均来自北京、上海、深圳、成都、沈阳五地覆盖主流旅游区域。此过程可在 2 秒内完成比 1000 条 INSERT 快 10 倍以上。5.2 Vue 路由守卫实现角色动态菜单游客/导游/管理员视图隔离“飞鸽系统”要求不同角色看到不同菜单栏。不推荐在router/index.js中硬编码meta: { roles: [admin] }而应从后端返回菜单结构后端GET /api/user/menu返回{ code: 0, data: [ { path: /home, name: 首页, icon: HomeOutlined }, { path: /orders, name: 我的订单, icon: ShoppingCartOutlined }, { path: /admin/spots, name: 景区管理, icon: EnvironmentOutlined, roles: [admin] } ] }前端在router/index.js中动态添加路由import { createRouter, createWebHistory } from vue-router; import { getUserMenu } from /api/user; const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) } ] }); // 全局前置守卫登录后拉取菜单并 addRoute router.beforeEach(async (to, from, next) { if (to.path /login) { next(); return; } const token localStorage.getItem(token); if (!token) { next(/login); return; } try { const menuRes await getUserMenu(); menuRes.data.forEach(item { if (item.roles item.roles.includes(admin)) { router.addRoute({ path: item.path, name: item.name, component: () import(/views${item.path}.vue) }); } }); next(); } catch (error) { next(/login); } }); export default router;关键点router.addRoute()动态注册路由确保未授权角色无法访问/admin/spotsimport()语法实现路由懒加载避免首屏加载过大。此方案比v-if控制菜单显示更安全——即使用户手动修改 DOM也无法绕过路由守卫。5.3 MySQL 自动备份脚本用 BAT 文件实现每日凌晨 2 点导出高分项目需体现运维意识。在 Windows 服务器上创建backup_flypigeon.batecho off set BACKUP_DIRD:\backup\flypigeon set DATESTR%date:~0,4%%date:~5,2%%date:~8,2% set TIMESTR%time:~0,2%%time:~3,2%%time:~6,2% set TIMESTR%TIMESTR: 0% if not exist %BACKUP_DIR% mkdir %BACKUP_DIR% mysqldump -u root -pYourPassword flypigeon %BACKUP_DIR%\flypigeon_%DATESTR%_%TIMESTR%.sql :: 删除 7 天前的备份 forfiles /p %BACKUP_DIR% /d -7 /c cmd /c del path将该 BAT 文件加入 Windows 任务计划程序设置每天 2:00 AM 运行。备份文件命名含日期时间便于追溯forfiles命令自动清理过期文件避免磁盘占满。提示密码明文存在风险生产环境应使用 MySQL 配置文件my.cnf存储凭据并设置chmod 600 my.cnf。教学项目中此 BAT 脚本已足够体现“数据资产保护”意识是答辩时加分项。本文还有配套的精品资源点击获取
返回列表