基于Node.js+Vue+Express的旅游景点推荐系统全栈开发实践
发布时间:2026/9/26 13:46:39来源:尧图网络
看到基于nodejsvueexpress的旅游景点推荐系统_41asw896这个项目名基本能把工作量猜个七八成一个典型的前后端分离Web应用后端用Node.js Express扛接口前端用Vue渲染页面业务重点放在景点推荐上。这类项目在毕业设计、课程设计和个人作品集里出现频率极高技术栈经典、业务场景清晰、可扩展空间也大非常适合用来打通全栈开发的完整流程。今天我就从零到一把这个项目拆开讲讲从环境配置到前后端联调再到推荐算法怎么落地把我在实际开发中踩过的坑和觉得有价值的细节都写出来希望对正在做类似项目或想入门全栈的朋友有帮助。先说结论这个项目适合三类人——想用最短时间走通数据库 后端接口 前端页面全流程的初学者需要一份结构完整、有业务亮点的毕设/课设项目的学生以及想把推荐系统这个概念落地成代码但不想直接上Spark、Hadoop那套重型框架的开发者。项目规模不大不小正好能承载数据建模 接口设计 算法简化 前后端交互这几个核心环节做完之后你对整个Web开发链路的理解会清晰很多。1. 项目定位与整体设计思路1.1 为什么选Node.js Vue Express这套组合这个技术栈放在今天依然是中小型全栈项目里最舒服的组合之一。Node.js作为运行环境让JavaScript从浏览器走进了后端Express是Node生态里最经典的Web框架中间件机制简单直观路由写法贴近HTTP本身学习曲线非常平缓Vue则负责前端界面组件化开发配合响应式数据绑定写页面效率很高中文资料和社区生态都极其成熟遇到问题搜一下基本都有答案。对比一下其他方案你就明白怎么选了。用Java SpringBoot写后端当然也行但对刚入门的人来说Maven依赖管理、Tomcat部署、Java语法本身都是额外负担把精力耗在环境上不值当。用Python Flask/Django也算顺手但如果你想保持前后端一种语言的统一体验那Node.js有天然优势——前端写组件、后端写接口用的都是JavaScript/TypeScript上下文切换成本低。Express相比Koa和NestJS最大优势就是没有那么多条条框框项目结构你可以自己定适合做教学型和个人项目NestJS有依赖注入和模块化约束适合大型团队协作做毕设反而显得笨重。1.2 旅游景点推荐系统的业务核心是什么这个项目的业务场景可以拆成三个层次。最底层是景点的增删改查CRUD这是所有功能的地基包括景点名称、所在城市、分类标签、封面图片、简介、评分、浏览热度等基础字段。中间层是用户系统注册、登录、收藏、浏览记录这些功能服务于一个目的——收集用户的行为数据。最上层才是推荐系统根据用户的搜索记录、收藏偏好、评分数据把可能感兴趣的景点推到用户面前。很多初学者容易在这里犯一个错把推荐系统想得特别高大上一上来就想用协同过滤、矩阵分解、深度学习。但实际上90%的课设级推荐系统用基于标签的内容匹配 热度加权就能跑出不错的效果。这个项目最聪明的做法是先实现一个规则清楚、逻辑可解释的轻量推荐引擎再根据业务数据的丰富程度逐步升级。先把推荐逻辑做出来后续想往协同过滤、混合推荐方向扩展代码架构上留好接口就行。1.3 前后端分离架构下的模块划分既然选了Vue Express前后端分离就是默认方案。前端工程管理界面渲染、用户交互、路由跳转通过HTTP请求访问后端接口后端工程负责业务逻辑、数据校验、数据库操作、鉴权。两者通过JSON格式的数据通信开发阶段用代理解决跨域生产阶段由后端托管前端打包产物。我建议把项目按这样的模块划分来组织模块功能说明涉及技术点用户模块注册、登录、Token鉴权bcrypt密码加密、JWT景点模块景点列表、详情、分类筛选分页查询、条件拼接收藏模块收藏/取消收藏、收藏列表联合唯一索引、联表查询推荐模块热门推荐、个性化推荐标签匹配、热度加权、简化协同过滤前端展示首页推荐流、景点详情、个人中心Vue Router、Axios、状态管理这样的模块划分保证了每一块的职责单一后面对照着调Bug、加功能都很方便。2. 环境搭建与工程初始化2.1 Node.js安装和环境变量配置全流程这个项目的一切都建立在Node.js环境之上所以安装环节必须一次搞定。去官网下载LTS版本安装包Windows下直接一路Next就行安装完成后Node.js会把自身路径追加到系统PATH变量中。验证是否安装成功打开命令行输入node -v能看到版本号就说明基础环境没问题。这里要特别说一个高频安装报错热搜词里反复出现的npm : 无法加载文件 ...npm.ps1因为在此系统上禁止运行脚本就是它。这个问题的根源是Windows PowerShell默认的执行策略ExecutionPolicy是Restricted禁止运行任何.ps1脚本而npm在Windows下恰恰是通过npm.ps1这个PowerShell脚本启动的。解决方式有三种任选其一即可以管理员身份打开PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser之后选择Y确认。RemoteSigned允许运行本地脚本对于从网上下载的脚本则要求有数字签名。不想改策略的话直接使用CMD而不是PowerShell来执行npm命令CMD没有这道限制。更推荐的方案是改用nvm-windows来管理Node版本它内部处理了这些环境变量问题还能随时切换Node版本遇到老旧项目需要降版本时尤其好用。还有个容易被忽略的细节npm的全局安装路径和缓存路径默认在C盘用户目录下包装多了C盘会吃紧。建议执行下面两条命令把路径改到其他盘npm config set prefix D:\nodejs_global npm config set cache D:\nodejs_cache改完之后记得手动创建目录并把D:\nodejs_global添加到系统PATH否则全局安装的命令比如vue命令会找不到。2.2 用Vite创建Vue前端工程前端工程创建方式有两种主流选择取决于你的Node版本和项目兼容要求。新项目我建议直接用npm create vuelatest它会基于Vite构建默认生成Vue 3 Vite 组合式API的项目骨架启动速度快、依赖少、工程结构清爽npm create vuelatest web cd web npm install npm run dev执行过程中会询问是否安装TypeScript、Vue Router、Pinia、ESLint等按需选就行。这个项目建议勾选Vue Router和Pinia一个是路由跳转必须的一个用于管理登录态和用户信息。如果你的课程要求用的是Vue 2那就需要走Webpack路线npm install -g vue/cli安装全局脚手架然后vue create web创建项目。这里有个坑要提前预防Vue CLI创建的项目默认依赖node-sass编译样式而node-sass对Node版本有严格限制Node 17以上版本基本无法安装。解决办法是改用dart-sass或者在package.json里把sass依赖手动替换成sassdart-sass版本写法上略有不同但功能一致。说实话现在没有特殊情况直接用Vite Vue 3就好少受很多罪。2.3 初始化Express后端工程后端工程不需要脚手架从零手动搭建更能理解每一步的意义。创建server目录执行npm init -y生成package.json然后安装核心依赖npm install express mysql2 cors jsonwebtoken bcryptjs dotenv简单说说这些依赖各自的角色。express提供HTTP路由和中间件机制mysql2是MySQL数据库驱动支持Promise异步写法cors解决开发阶段的跨域限制jsonwebtoken生成和校验JWT令牌bcryptjs做密码哈希加密选择js版本是为了避免node-gyp编译问题有些机器装原生bcrypt会失败dotenv负责从.env文件加载环境变量把数据库密码、端口号这类敏感配置跟代码分离。后端工程目录结构建议这样组织server/ app.js // 应用入口中间件注册、路由挂载 .env // 环境变量配置 routes/ // 路由定义按模块拆文件 auth.js attractions.js favorites.js recommend.js controllers/ // 业务逻辑处理函数 middleware/ // 鉴权、错误处理等中间件 db/ index.js // 数据库连接池 utils/ response.js // 统一返回格式封装在app.js里最核心的几件事加载环境变量、注册cors和express.json两个中间件、挂载路由、配置静态资源目录和404处理。3. 后端核心数据模型与推荐逻辑实现3.1 数据库表设计与建表SQL数据库层面我选择MySQL理由是这个系统里所有数据都是结构化、关系型的用它管理再合适不过。核心表有四张景点表、用户表、收藏表、评分表。设计时用心一点后面写推荐逻辑会省很多事。景点表必须覆盖后来做推荐需要的所有字段CREATE TABLE attractions ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景点名称, city VARCHAR(50) NOT NULL COMMENT 所在城市, category VARCHAR(50) COMMENT 分类自然风光/人文古迹/主题乐园等, tags VARCHAR(255) COMMENT 标签逗号分隔如:徒步,亲子,摄影, cover_url VARCHAR(255) COMMENT 封面图地址, description TEXT COMMENT 景点简介, score DECIMAL(2,1) DEFAULT 5.0 COMMENT 用户评分, visit_count INT DEFAULT 0 COMMENT 访问热度, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );用户表和收藏表是推荐系统的数据来源收藏表特别要注意加联合唯一索引防止同一用户重复收藏同一景点CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, preferences VARCHAR(255) COMMENT 用户偏好标签逗号分隔, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE favorites ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, attraction_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_attraction (user_id, attraction_id) );除此之外可以加一张浏览记录表记录用户每次点击景点详情的行为推荐系统里近期浏览过哪些景点是比收藏更细粒度的偏好信号。评分表则记录用户对景点的星级评分评分数据既能用于推荐加权也能让景点综合评分更有依据。3.2 Express路由设计与接口规范路由设计遵循RESTful风格每个模块一组路由按功能拆文件挂在app.js上。核心接口列表如下方法路径功能是否鉴权POST/api/auth/register用户注册否POST/api/auth/login用户登录否GET/api/attractions景点列表筛选否GET/api/attractions/:id景点详情否POST/api/favorites/:attractionId收藏景点是DELETE/api/favorites/:attractionId取消收藏是GET/api/favorites收藏列表是GET/api/recommendations个性化推荐可选鉴权中间件是后端最核心的复用逻辑它的任务是从请求头Authorization字段取出Bearer Token用jsonwebtoken验证签名解析出userId挂到req对象上供后续控制器使用。代码大致长这样const jwt require(jsonwebtoken); module.exports function authMiddleware(req, res, next) { const token req.headers.authorization?.split( )[1]; if (!token) return res.status(401).json({ code: 401, message: 未登录 }); try { const payload jwt.verify(token, process.env.JWT_SECRET); req.userId payload.userId; next(); } catch (e) { return res.status(401).json({ code: 401, message: 登录已过期 }); } };密码存储必须用bcryptjs做哈希绝不能明文存数据库。注册时bcrypt.hashSync(password, 10)生成哈希串登录时用bcrypt.compareSync(password, hash)校验。这里的10是salt rounds数值越大计算越慢、安全性越高10到12这个范围在实际项目中性价比最好。mysql2使用连接池管理数据库连接避免频繁创建连接导致性能下降配置方式如下const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, charset: utf8mb4 });这里有个很容易踩的坑连接参数里不显式指定charset为utf8mb4插入中文时大概率出现乱码。utf8mb4是MySQL的完整UTF-8实现能正确存储emoji和生僻字建库时也要指定CREATE DATABASE travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。3.3 推荐算法的简化落地从规则到协同过滤这是整个项目技术含量最高的部分也是答辩时最能讲出东西的地方。我把推荐实现分了三个层级每一层都在上一层的逻辑上扩展你可以根据自己的时间精力选做哪几层。第一层是排行榜式推荐也叫冷启动推荐。当系统没有任何用户行为数据或当前用户是刚注册的新用户时直接返回热门景点作为默认推荐。热门程度的计算不能只看评分也不能只看访问量更合理的做法是加权综合推荐指数 评分归一化值 * 0.6 访问热度归一化值 * 0.4。归一化就是把数据映射到0到1之间避免访问量上万而评分只有4.0这种量纲差异导致的结果失衡。SQL里可以直接这样写SELECT *, (score / 5 * 0.6 visit_count / (SELECT MAX(visit_count) FROM attractions) * 0.4) AS hot_score FROM attractions ORDER BY hot_score DESC LIMIT 10;第二层是基于标签的内容推荐。系统从用户收藏过的景点、浏览记录中提取出现频次最高的标签把这些标签作为用户的偏好画像然后从景点表里查找覆盖这些标签的景点。实现思路用户收藏了丽江古城标签古镇、慢生活、摄影系统就给用户推荐同样带古镇标签的乌镇平遥古城。SQL用LIKE %模糊匹配虽然不优雅但在这个数据量级下完全够用而且直观易懂。第三层才是协同过滤。这个方向的经典问题是寻找和你口味相似的人推荐他们喜欢但你还没看过的东西实现路径分两步。第一步找到相似用户SQL思路是查所有和当前用户有共同收藏景点的人按共同收藏数量排序SELECT f2.user_id, COUNT(*) AS common_count FROM favorites f1 JOIN favorites f2 ON f1.attraction_id f2.attraction_id AND f2.user_id ! f1.user_id WHERE f1.user_id ? GROUP BY f2.user_id ORDER BY common_count DESC LIMIT 5;第二步从这些相似用户的收藏列表里找出当前用户没有收藏过的景点按被收藏次数降序推荐。这个方案不需要引入额外的相似度计算框架用两条SQL就能实现对课设项目来说是性价比极高的亮点功能。当然你可以把这些结果合并成一个推荐池再做一次热度加权排序实际效果会更好。合理的设计如下已登录用户优先返回个性化推荐第二层第三层合并去重未登录或数据不足时走第一层热门推荐。这里要注意的是要剔除掉用户已经收藏过的景点否则推荐列表里出现旧收藏会显得算法很蠢。后端实现时用一个NOT IN子查询排除即可。4. 前端核心页面结构与交互实现4.1 路由设计、页面拆解与组件划分前端部分是Vue的主场。对于这个项目路由配置是最先要做的事因为它决定了整个页面的骨架const routes [ { path: /, name: home, component: () import(../views/HomeView.vue) }, { path: /attraction/:id, name: detail, component: () import(../views/DetailView.vue) }, { path: /favorites, name: favorites, component: () import(../views/FavoritesView.vue) }, { path: /login, name: login, component: () import(../views/LoginView.vue) }, { path: /register, name: register, component: () import(../views/RegisterView.vue) } ];这里使用了动态import做路由级代码分割组件按需加载而不是一次性打包能明显改善首屏加载速度。路径/attraction/:id中:id是动态参数页面中通过route.params.id获取跳转时用router.push({ path: /attraction/ id })这就对应了热搜词里vue路由参数那个点。页面组件拆分的思路是保持每个组件职责单一。我建议至少拆出这些NavBar顶部导航栏、AttractionCard景点卡片在列表和推荐模块中复用、SearchBar搜索筛选栏、SkeletonList骨架屏加载占位、RatingStars星级评分展示。AttractionCard这个组件尤其值得用心设计因为它在首页、收藏页、推荐模块里都要用到抽出来只写一次后面要加收藏按钮状态切换只需改一个地方。4.2 Axios请求封装与拦截器前端所有HTTP请求都应该走一个统一封装的请求模块而不是在每个组件里直接调用axios。这样做的直接好处是统一管理BaseURL、统一携带Token、统一处理错误码。封装的核心逻辑如下import axios from axios; import { useUserStore } from /stores/user; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const userStore useUserStore(); if (userStore.token) { config.headers.Authorization Bearer userStore.token; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { // 登录过期跳转登录页 router.push(/login); } return Promise.reject(error); } );请求拦截器里做Token注入响应拦截器里做统一的错误处理特别是401状态码自动跳转登录页。这样每个业务模块里写请求时就不用重复处理这些逻辑了代码干净很多。4.3 首页推荐流与景点详情的交互实现首页是整个项目的门面。布局上建议分为三块顶部搜索栏、筛选标签栏、推荐景点列表。搜索栏按景点名或城市关键词模糊查询筛选标签栏按分类自然风光、人文古迹、主题乐园、红色旅游等和城市做条件过滤。筛选条件变化时重新请求接口保持URL查询参数和页面状态同步。推荐模块放在首页最核心的位置登录状态下请求个性化推荐接口瀑布流式渲染推荐卡片未登录时显示热门景点榜并在推荐区域加一行提示登录后查看个性化推荐。这种设计在交互上很自然也能让用户直观感受到推荐系统的存在。景点详情页是另一个重点页面。通过route.params.id调取详情接口展示封面大图、景点名称、所在城市、分类标签、评分、访问量、完整介绍。页面下方放收藏按钮登录用户可以点击收藏或取消收藏按钮状态实时切换。地图展示可以嵌入高德地图或腾讯地图的JavaScript API根据景点的经纬度坐标在页面中渲染一张静态地图这个功能在答辩展示时视觉上加分很明显。如果景点有宣传视频还可以用video.js加hls.js实现m3u8流媒体视频播放增加页面的丰富度。5. 联调、打包与部署要点5.1 开发阶段跨域问题的两种解法前后端分离开发时前端跑在5173端口Vite默认后端跑在3000端口浏览器直接跨域请求会被拦截。解法有两个前端代理或者后端CORS。我更推荐前端代理因为后端接口部署到生产环境后本来就不该无条件开放CORS。Vite配置代理的方法是在vite.config.js里加server.proxy配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } });这样前端请求/api/attractions会被代理转发到http://localhost:3000/api/attractions浏览器看到的还是同源请求不需要后端做任何跨域处理。如果选择后端CORS方案则需要安装cors中间件后app.use(cors())但默认是允许所有来源生产环境最好限制为具体域名。5.2 生产环境构建与Express静态托管开发联调完成后就是打包上线。前端执行npm run build生成dist目录里面是静态HTML/CSS/JS文件。最简单的部署方式就是让Express托管这些静态文件同时把前端路由的history模式回退配置好const path require(path); app.use(express.static(path.join(__dirname, public))); app.get(*, (req, res) { res.sendFile(path.join(__dirname, public, index.html)); });这里app.get(*)这个通配路由特别关键。Vue Router的history模式不依赖hash跳转到/attraction/1这类页面时刷新浏览器服务器如果找不到对应静态资源就会404有了这个回退路由所有路径都会回到index.html再由前端路由接管渲染。如果你用的是Express 5通配符语法改成了app.get(/*splat, ...)这是版本差异带来的坑注意检查package.json里的版本号。5.3 使用PM2或Nginx做生产部署增强如果要把项目部署到云服务器上我推荐在Express前面再加一层Nginx做反向代理。Nginx负责监听80端口、处理静态资源请求、把/api/前缀的请求转发给Node进程Node进程则用PM2守护崩溃自动重启。一个典型的Nginx站点配置如下server { listen 80; server_name your_domain.com; location / { root /var/www/travel-web; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; } }这样的部署结构把静态资源和动态接口彻底分离Nginx处理高并发静态请求的能力比Node的静态中间件强太多PM2则保证Node服务进程永远在线。这套架构虽然是小项目的配置但已经是大厂部署的最小模型做完之后简历上可以理直气壮地写掌握前后端分离项目的生产部署流程。6. 常见问题速查与避坑记录这里把我在实际开发中遇到的、以及身边同学反复踩过的坑集中整理成一张速查表。这些问题分布在环境配置、前后端联调、数据库、部署等各个环节遇到同样报错时直接对照排查就行。现象根本原因解决办法npm命令在PowerShell报禁止运行脚本Windows执行策略限制.ps1脚本Set-ExecutionPolicy RemoteSigned -Scope CurrentUser后端接口返回中文乱码MySQL连接charset未指定utf8mb4连接参数加charset: utf8mb4建库指定utf8mb4前端请求接口报CORS跨域错误浏览器同源策略拦截使用Vite devServer代理路径前缀/api转发到后端端口3000被占用上一次运行未退出查端口进程netstat -anoMySQL连接报1045错误用户名/密码错误核对.env配置确认账号有远程访问权限MySQL连接报ECONNREFUSED数据库服务未启动检查MySQL服务状态Windows服务管理器或systemctl打包打开页面空白Vue history路由直接访问子路径无回退Express配置app.get(*)回退到index.htmlPromise.all里一个请求失败导致整体异常错误处理不完善用Promise.allSettled替代或者分开请求分别catch收藏按钮重复点击报错未处理重复收藏数据库联合唯一索引兜底前端请求防重复点击这里着重提示两个最常见的坑。第一个是PowerShell执行策略问题表面看是npm命令无法使用实际上是你还没开始写代码就卡在了环境上建议把它当成环境配置的第一课来对待。第二个是Vue history模式打包后空白这个问题几乎每个做过前端部署的人都遇到过一定要确认服务端有没有做路由回退如果用的是Nginx就得配try_files如果纯用Express托管就得配通配路由。还有一个容易被忽略的细节是图片资源路径。景点封面图如果存的是相对路径前端页面在不同路由层级下显示会出问题。最稳妥的做法是在上传图片时保存完整可访问路径或者统一用绝对路径以/api开头让后端静态托管避免在页面里拼接相对路径时多了一层目录就404。JWT过期处理也值得提前设计。如果只在前端响应拦截器里做401跳转用户正在详情页浏览时突然被踢到登录页体验很差。可以加一个自动续期的逻辑请求返回401时利用refreshToken刷新令牌重新请求一次刷新也失败才跳登录页。这个机制不算复杂但属于明显的加分细节答辩时可以主动提。数据库连接数问题也要留心。mysql2默认连接池如果没设置好并发请求多时会报Too many connections。一方面在createPool里限制connectionLimit另一方面每条SQL查询执行完要确保释放连接。用mysql2/promise配合const [rows] await pool.query()这种写法连接池会自动管理连接释放不会泄漏。最后说一个项目结构层面的心得不要把所有的路由和控制器都写在app.js一个文件里。刚开始图省事后面接口多了每次查找修改都很痛苦。按模块拆开之后改收藏模块就只动favorites相关的文件逻辑清晰定位Bug也快。这套模块化思想不光适用于这个项目也是所有后端工程的基本功。做完这个项目我对全栈开发的整体认知提升非常大。从数据表设计到接口联调从推荐算法简化到生产部署每个环节都亲手过了一遍很多书本上抽象的概念都有了具象的对应物。如果后续想继续扩展可以往这些方向走接入真实景点数据源、把推荐算法升级成基于物品的协同过滤、加一个后台管理页面做数据可视化大屏甚至把系统改造成支持多语言的游客版。核心代码架构打好了这些扩展都只是时间问题而不是技术风险。
网站建设高端定制企业官网