Node.js+Vue+MySQL旅游系统全栈实战指南
简介这是一套基于Node.jsVueMySQL开发的飞鸽旅游服务管理系统面向计算机专业本科生及前端/全栈初学者适用于毕业设计、课程设计与期末大作业等实践场景。系统采用前后端分离架构涵盖用户管理、订单处理、服务评价、旅游资源维护等核心功能界面美观、操作直观、部署即用。资源包共300个文件含82个Vue组件文件实现动态交互界面、76个JS逻辑脚本含Node后端路由与API、46个PNG与35个JPG图片资源含UI图标与页面素材、19个CSS样式文件及1个SQL数据库脚本适配MySQL 5.7整体压缩包大小为61.13MB采用RAR格式封装。已有67人学习下载资源提供完整可运行源码、数据库初始化脚本、VSCode开发环境配置说明、Navicat数据库连接指南及配套bat启动脚本无需二次修改即可快速部署调试显著降低全栈项目实践门槛。1. 飞鸽旅游服务管理系统一个典型的 Node.js Vue MySQL 全栈旅游业务系统不是 Demo而是可落地的高分级工程实践“飞鸽旅游服务管理系统”这个名字听起来像校园课设但实际在高校毕业设计、企业内部培训系统、中小型旅行社数字化改造场景中它代表一类真实存在的业务系统支持游客在线浏览线路、提交订单、查看行程单管理员能审核订单、管理地接资源、维护景点与酒店信息、生成统计报表。它之所以被标记为“高分项目”不是因为用了多少炫技功能而是因为结构清晰、职责分离明确、前后端通信规范、数据库设计贴合旅游业务实体关系——比如「线路」与「出发团期」一对多、「订单」关联「游客清单」和「保险选项」、「供应商」与「地接社」存在分级协作关系。这类系统对开发者的要求很实在Node.js 要稳住接口层并发与事务控制Vue 要处理好表单联动如选择出发城市后动态加载可选线路、分页列表与状态管理MySQL 则必须支撑多条件组合查询如按日期价格区间主题标签筛选线路和高频读写平衡。适合刚完成全栈入门、正准备用真实业务逻辑检验技术栈整合能力的开发者也适合作为团队内技术规范落地的参考样板。2. 搭建开发环境从零配置 Node.js Vue MySQL绕过 npm.ps1 权限报错等高频阻塞点2.1 安装 Node.js 并修复 PowerShell 执行策略导致的 npm 报错Windows 系统下安装 Node.js 后常遇到npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本错误。这不是 Node.js 安装失败而是 Windows 默认禁止执行本地 PowerShell 脚本的安全策略所致。不要卸载重装只需在管理员权限的 PowerShell 中执行以下命令Set-ExecutionPolicy RemoteSigned -Scope CurrentUser该命令将当前用户范围的执行策略设为RemoteSigned允许运行本地脚本如 npm.ps1同时要求从互联网下载的脚本必须带有效签名兼顾安全与可用性。执行后关闭并重新打开终端node -v和npm -v即可正常输出版本号。若仍报错检查是否在非管理员模式下运行 PowerShell —— 此操作必须以“以管理员身份运行”启动。提示Set-ExecutionPolicy修改的是当前用户的策略不影响系统其他账户无需担心全局风险。若后续需恢复默认策略执行Set-ExecutionPolicy Undefined -Scope CurrentUser即可。2.2 创建 Vue 前端项目并启用 Router 与 Axios 支持使用 Vue 官方脚手架create-vue替代已废弃的 vue-cli初始化项目命令如下npm create vuelatest交互式向导中务必勾选Add TypeScript?→ No除非团队强制要求本项目以 JavaScript 为主降低理解门槛Add JSX Support?→ NoAdd Vue Router for Single Page Application?→ Yes旅游系统必然需要多页面跳转如/home、/line/list、/order/createAdd Pinia for state management?→ Yes替代 Vuex轻量且类型友好用于存储用户登录态、购物车临时数据Add Vitest for Unit Testing?→ No高分项目侧重功能完整与业务逻辑非测试覆盖率项目生成后进入目录安装 Axios 并配置基础请求拦截器cd flypigeon-frontend npm install axios在src/utils/request.js中编写统一请求封装import axios from axios // 创建 axios 实例 const request axios.create({ baseURL: /api, // 与后端代理路径一致避免跨域 timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use( config { const token localStorage.getItem(auth_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) ) // 响应拦截器统一错误处理 request.interceptors.response.use( response response.data, // 直接返回 data 字段简化组件调用 error { if (error.response?.status 401) { localStorage.removeItem(auth_token) window.location.href /login } return Promise.reject(error) } ) export default request此封装使所有 API 调用只需request.get(/lines)无需重复处理 token 和 401 跳转是旅游系统中用户会话管理的关键基础设施。2.3 配置 MySQL 8.0 并创建符合旅游业务的初始库表下载 MySQL Community Server 8.0推荐 ZIP 免安装版避免 MSI 安装器在 Win11 上的兼容问题解压后手动初始化# 进入 bin 目录执行初始化生成 root 密码 mysqld --initialize --console # 启动服务需先安装 Windows Service或直接运行 mysqld.exe mysqld --install MySQL80 net start MySQL80使用初始密码登录后立即创建业务专用数据库与用户-- 创建数据库指定 utf8mb4 字符集支持 emoji 及生僻地名 CREATE DATABASE flypigeon DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建应用专用用户限制仅本地访问赋予最小必要权限 CREATE USER flypigeon_applocalhost IDENTIFIED BY SecurePass2024!; GRANT SELECT, INSERT, UPDATE, DELETE ON flypigeon.* TO flypigeon_applocalhost; FLUSH PRIVILEGES;接着执行建表语句。旅游系统核心表之一是t_line旅游线路其设计需体现业务约束USE flypigeon; CREATE TABLE t_line ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 线路标题如【云南全景·7日深度游】, price DECIMAL(10,2) NOT NULL COMMENT 成人价单位元, departure_city VARCHAR(50) NOT NULL COMMENT 出发城市如北京、上海, days TINYINT NOT NULL DEFAULT 7 COMMENT 行程天数, status ENUM(draft,online,offline) DEFAULT draft COMMENT 发布状态, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT旅游线路主表; -- 为高频查询字段添加复合索引按出发城市状态价格排序 CREATE INDEX idx_city_status_price ON t_line (departure_city, status, price);该索引直接服务于前端「按城市筛选线路」「只看已上线线路」「按价格从低到高排序」的典型查询场景避免全表扫描是 MySQL 层性能优化的第一步。3. 实现核心业务模块用 Express 构建 RESTful 接口Vue 页面驱动旅游线路 CRUD 流程3.1 Node.js 后端基于 Express 的线路管理接口含分页与状态过滤使用 Express 搭建轻量后端不引入 Koa 或 NestJS聚焦业务逻辑表达。首先初始化项目结构mkdir flypigeon-backend cd flypigeon-backend npm init -y npm install express mysql2 cors dotenv npm install --save-dev nodemon在server.js中配置数据库连接池与路由const express require(express) const mysql require(mysql2/promise) const cors require(cors) require(dotenv).config() const app express() app.use(cors()) // 允许前端跨域请求 app.use(express.json()) app.use(express.urlencoded({ extended: true })) // 创建 MySQL 连接池复用连接避免频繁创建销毁 const pool mysql.createPool({ host: process.env.DB_HOST || localhost, port: process.env.DB_PORT || 3306, user: process.env.DB_USER || flypigeon_app, password: process.env.DB_PASSWORD || SecurePass2024!, database: process.env.DB_NAME || flypigeon, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }) // 线路列表接口支持分页、城市筛选、状态过滤 app.get(/api/lines, async (req, res) { const { page 1, limit 10, city, status } req.query const offset (page - 1) * limit let sql SELECT id, title, price, departure_city, days, status FROM t_line WHERE 11 const params [] if (city) { sql AND departure_city ? params.push(city) } if (status) { sql AND status ? params.push(status) } sql ORDER BY created_at DESC LIMIT ? OFFSET ? params.push(parseInt(limit), parseInt(offset)) try { const [rows] await pool.execute(sql, params) const [total] await pool.execute(SELECT COUNT(*) as count FROM t_line WHERE 11 (city ? AND departure_city ? : ) (status ? AND status ? : ), city || status ? [city, status].filter(Boolean) : []) res.json({ code: 0, data: rows, pagination: { total: total[0].count, page: parseInt(page), limit: parseInt(limit) } }) } catch (err) { console.error(err) res.status(500).json({ code: 500, message: 服务器内部错误 }) } }) app.listen(3000, () { console.log(Backend server running on http://localhost:3000) })关键点说明mysql2/promise提供await pool.execute()避免回调地狱代码线性可读分页参数page/limit由前端传入后端计算offset符合 RESTful 规范WHERE 11是安全拼接条件的惯用写法避免判断空条件时 SQL 语法错误COUNT(*)查询独立执行确保总数准确不因LIMIT影响。3.2 Vue 前端用 Composition API 实现线路列表页与搜索联动在src/views/LineListView.vue中实现响应式列表与搜索template div classline-list h2旅游线路管理/h2 !-- 搜索栏 -- div classsearch-bar input v-modelsearch.city placeholder出发城市 / select v-modelsearch.status option value全部状态/option option valueonline已上线/option option valueoffline已下线/option /select button clickfetchLines搜索/button button clickresetSearch重置/button /div !-- 列表 -- table classline-table thead tr th标题/th th出发地/th th价格/th th天数/th th状态/th th操作/th /tr /thead tbody tr v-forline in lines :keyline.id td{{ line.title }}/td td{{ line.departure_city }}/td td¥{{ line.price }}/td td{{ line.days }}天/td td span :class[status-badge, line.status]{{ statusText[line.status] }}/span /td td button clickeditLine(line)编辑/button button clickdeleteLine(line.id)删除/button /td /tr /tbody /table !-- 分页 -- div classpagination button clickchangePage(currentPage - 1) :disabledcurrentPage 1上一页/button span第 {{ currentPage }} 页共 {{ totalPages }} 页/span button clickchangePage(currentPage 1) :disabledcurrentPage totalPages下一页/button /div /div /template script setup import { ref, onMounted, watch } from vue import request from /utils/request const lines ref([]) const pagination ref({ total: 0, page: 1, limit: 10 }) const currentPage ref(1) const search ref({ city: , status: }) const statusText { draft: 草稿, online: 已上线, offline: 已下线 } // 获取线路列表 const fetchLines async () { try { const res await request.get(/lines, { params: { page: currentPage.value, limit: pagination.value.limit, city: search.value.city, status: search.value.status } }) lines.value res.data pagination.value res.pagination } catch (err) { console.error(获取线路失败:, err) } } // 重置搜索条件 const resetSearch () { search.value { city: , status: } currentPage.value 1 fetchLines() } // 切换页码 const changePage (page) { if (page 1 page pagination.value.totalPages) { currentPage.value page fetchLines() } } // 监听搜索条件变化自动触发搜索 watch(search, () { currentPage.value 1 fetchLines() }, { deep: true }) // 组件挂载时加载第一页 onMounted(() { fetchLines() }) /script style scoped .line-table { width: 100%; border-collapse: collapse; } .line-table th, .line-table td { border: 1px solid #ddd; padding: 8px; text-align: left; } .status-badge { padding: 2px 8px; border-radius: 4px; font-size: 12px; } .status-badge.online { background-color: #4CAF50; color: white; } .status-badge.offline { background-color: #f44336; color: white; } .status-badge.draft { background-color: #ff9800; color: white; } /style此组件体现了 Vue 3 Composition API 的核心优势ref管理响应式状态、watch监听深层对象变化、onMounted控制生命周期。搜索栏输入即触发fetchLines无需额外按钮点击提升用户体验 —— 这正是旅游系统中「快速筛选线路」这一高频操作的合理交互设计。3.3 前后端联调验证用 curl 模拟请求确认接口返回符合预期在开发过程中脱离前端直接验证后端接口是定位问题最快的方式。使用curl发送请求观察原始响应# 获取前10条已上线的北京出发线路 curl http://localhost:3000/api/lines?page1limit10city北京statusonline # 响应示例格式化后 { code: 0, data: [ { id: 101, title: 【北京直飞】云南全景·7日深度游, price: 5999.00, departure_city: 北京, days: 7, status: online } ], pagination: { total: 42, page: 1, limit: 10 } }若返回500错误检查 Node.js 控制台报错堆栈常见原因包括MySQL 用户无SELECT权限执行SHOW GRANTS FOR flypigeon_applocalhost;验证t_line表不存在或字段名拼写错误DESCRIBE t_line;查看结构环境变量.env文件未正确加载console.log(process.env.DB_USER)调试。注意curl命令中的 URL 必须与 Vue 代理配置匹配。若前端使用 Vite 的server.proxy需确保vite.config.js中配置了export default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } })4. 数据库设计进阶为旅游业务建模「订单-游客-保险」三元关系规避 N1 查询陷阱4.1 识别旅游业务中的核心实体关系从 ER 图到具体表结构旅游订单远比电商订单复杂一个订单对应多个游客家庭出游、多种保险成人险/儿童险/意外险、多个服务项接送机、签证代办。若用单张t_order表硬塞所有字段将导致严重耦合与扩展困难。正确做法是拆分为四张表形成清晰的主从关系表名作用关键字段t_order订单主表id,order_no,user_id,total_amount,status,created_att_order_passenger游客明细一对多id,order_id,name,id_card,phone,is_adultt_order_insurance保险明细一对多id,order_id,insurance_type,price,insured_namet_order_service附加服务一对多id,order_id,service_name,price,remark其中t_order_passenger是典型的一对多关系一个订单可包含父母孩子共3人每人在t_order_passenger中占一行。这种设计使「查询某订单所有游客」变为简单 JOIN而非解析 JSON 字段。4.2 编写高效 JOIN 查询一次获取订单及其全部关联数据前端订单详情页需展示订单基本信息 所有游客姓名身份证 所购保险类型 附加服务列表。若分4次 API 调用会产生 N1 问题。应在后端提供聚合接口-- 一次性查出订单及关联数据MySQL 8.0 支持 JSON_AGG 模拟数组但此处用 LEFT JOIN 更通用 SELECT o.id AS order_id, o.order_no, o.total_amount, o.status, -- 游客列表GROUP_CONCAT 拼接适用于简单展示 GROUP_CONCAT( CONCAT(p.name, (, p.id_card, )) SEPARATOR ; ) AS passengers, -- 保险总金额 COALESCE(SUM(i.price), 0) AS insurance_total, -- 服务项数量 COUNT(s.id) AS service_count FROM t_order o LEFT JOIN t_order_passenger p ON o.id p.order_id LEFT JOIN t_order_insurance i ON o.id i.order_id LEFT JOIN t_order_service s ON o.id s.order_id WHERE o.id ? GROUP BY o.id;该查询用LEFT JOIN确保即使某订单无保险或服务也能返回订单基本信息GROUP_CONCAT将多名游客压缩为字符串避免结果集膨胀COALESCE处理SUM(NULL)返回NULL的情况。虽不如 NoSQL 的嵌套文档直观但在关系型数据库中是兼顾性能与可维护性的标准解法。4.3 在 Vue 中解析聚合数据用 computed 属性解构复杂响应假设后端/api/orders/:id返回上述聚合结果前端需将其还原为结构化数据供模板使用script setup import { ref, computed } from vue import request from /utils/request const order ref(null) const orderId 123 // 从路由参数获取 // 模拟后端返回的聚合数据 const rawOrder { order_id: 123, order_no: FP20240520001, total_amount: 12998.00, status: paid, passengers: 张三(110101199001011234); 李四(110101199202022345); 王五(110101201003033456), insurance_total: 320.00, service_count: 2 } // 解析乘客字符串为数组 const parsedPassengers computed(() { if (!rawOrder.passengers) return [] return rawOrder.passengers.split(; ).map(item { const match item.match(/^(.)\((.)\)$/) return match ? { name: match[1], idCard: match[2] } : { name: item, idCard: } }) }) // 计算保险人均费用总保险费 / 游客数 const avgInsurancePerPerson computed(() { const count parsedPassengers.value.length return count 0 ? (rawOrder.insurance_total / count).toFixed(2) : 0 }) /script template div classorder-detail h3订单 {{ rawOrder.order_no }}/h3 p总金额¥{{ rawOrder.total_amount }}/p h4游客信息/h4 ul li v-forp in parsedPassengers :keyp.idCard {{ p.name }}身份证{{ p.idCard }} /li /ul p保险人均费用¥{{ avgInsurancePerPerson }}/p /div /templatecomputed属性确保parsedPassengers和avgInsurancePerPerson在rawOrder变更时自动更新无需手动调用方法。这种声明式数据处理是 Vue 开发旅游系统时保持视图与状态同步的关键技巧。5. 生产部署前的关键检查环境变量隔离、MySQL 连接池调优、Vue 路由守卫防未授权访问5.1 用 .env 文件严格区分开发/生产环境配置杜绝密钥硬编码Node.js 项目必须将数据库密码、JWT 秘钥等敏感信息从代码中剥离。在项目根目录创建.env和.env.production# .env开发环境 DB_HOSTlocalhost DB_USERflypigeon_app DB_PASSWORDSecurePass2024! DB_NAMEflypigeon JWT_SECRETdev-jwt-secret-key-change-in-prod NODE_ENVdevelopment# .env.production生产环境部署时替换 DB_HOST10.0.1.5 DB_USERflypigeon_prod DB_PASSWORDProdPass!2024 DB_NAMEflypigeon JWT_SECRETprod-jwt-secret-32-byte-key-here NODE_ENVproduction在server.js中通过dotenv加载时需根据NODE_ENV自动选择文件const dotenv require(dotenv) if (process.env.NODE_ENV production) { dotenv.config({ path: .env.production }) } else { dotenv.config() }提示.env文件绝不能提交到 Git 仓库。务必在.gitignore中加入.env .env.local .env.*.local5.2 调整 MySQL 连接池参数应对旅游系统节假日期间的流量高峰旅游系统在春节、国庆等假期前会出现瞬时并发激增。默认连接池connectionLimit: 10可能成为瓶颈。需根据服务器内存与 MySQL 最大连接数调整参数推荐值说明connectionLimit20~50每个 Node.js 进程最多维持的连接数建议设为 MySQLmax_connections的 1/4waitForConnectionstrue连接池满时新请求等待而非直接报错queueLimit0等待队列无上限避免请求丢失需配合超时控制acquireTimeout10000获取连接最长等待 10 秒超时则返回错误修改后的连接池配置const pool mysql.createPool({ // ... 其他配置 connectionLimit: 30, waitForConnections: true, queueLimit: 0, acquireTimeout: 10000 })同时在 MySQL 侧检查并调大max_connections-- 查看当前最大连接数 SHOW VARIABLES LIKE max_connections; -- 临时调大重启失效 SET GLOBAL max_connections 200; -- 永久生效需修改 my.cnf 中 [mysqld] 下的 max_connections 2005.3 Vue 路由守卫拦截未登录用户访问/admin/*管理后台页面旅游系统的管理员后台如/admin/lines、/admin/orders必须强制登录。利用 Vue Router 的beforeEach全局守卫实现// src/router/index.js import { createRouter, createWebHistory } from vue-router import { useAuthStore } from /stores/auth const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: () import(/views/HomeView.vue) }, { path: /login, component: () import(/views/LoginView.vue) }, { path: /admin, component: () import(/layouts/AdminLayout.vue), children: [ { path: lines, component: () import(/views/AdminLineView.vue) }, { path: orders, component: () import(/views/AdminOrderView.vue) } ], meta: { requiresAuth: true } // 标记需认证 } ] }) router.beforeEach(async (to, from, next) { const authStore useAuthStore() // 若目标路由需要认证且用户未登录 if (to.meta.requiresAuth !authStore.token) { try { // 尝试从 localStorage 恢复 token 并验证有效性 const savedToken localStorage.getItem(auth_token) if (savedToken) { // 调用后端验证 token可选增强安全性 await authStore.validateToken(savedToken) } else { throw new Error(No token found) } } catch (err) { // 验证失败跳转登录页并记录原目标地址 next({ path: /login, query: { redirect: to.fullPath } }) return } } next() }) export default router此守卫确保任何对/admin/*的访问都经过 token 验证且支持「登录后跳回原页面」的体验通过redirect查询参数。这是旅游系统保障后台数据安全的最低防线。5.4 验证部署就绪用 curl 检查健康检查端点与静态资源路径生产环境部署后需快速验证服务是否真正就绪。在服务器上执行# 检查后端健康状态Express 添加 /health 端点 curl -I http://localhost:3000/health # 应返回 HTTP/1.1 200 OK # 检查前端静态资源是否可访问Vite 默认构建到 dist 目录 curl -I http://your-server-ip/static/js/index.123abc.js # 应返回 HTTP/1.1 200 OKContent-Type: application/javascript # 检查数据库连通性Node.js 进程内执行 echo SELECT 1 | mysql -u flypigeon_prod -pProdPass!2024 -h 10.0.1.5 flypigeon # 应输出 1三项检查全部通过才表明「飞鸽旅游服务管理系统」已具备上线条件。这比单纯看进程是否运行更可靠是运维交接时的标准验收动作。本文还有配套的精品资源点击获取