资讯动态

基于Node.js+Vue+Express的旅游景点推荐系统全栈开发实战

发布时间:2026/9/18 3:52:03 来源:尧图企业网站定制
开学答辩季又快到了每年这个时候都有不少同学卡在“旅游景点推荐系统”这个题目上。前几天就有个学弟问我能不能用 Node.js Vue Express 这套组合做一个既能交作业、又能写进简历的完整项目。我直接把之前的一个项目翻了出来就是“基于nodejsvueexpress的旅游景点推荐系统”。这套系统前端用 Vue 3 Element Plus后端用 Express数据库用的 MySQL是一个标准的前后端分离全栈项目能跑通注册登录、景点展示、推荐算法、后台管理等完整链路。我尽量把这套东西里的坑、关键代码、设计思路都写清楚给正准备做类似系统的朋友一个能落地的参考。为什么强调“能落地”因为这个题目看着不难但真正动手做的时候很多人会卡在环境配置、跨域、推荐算法怎么写、打包后布局异常这些地方最后项目跑不起来或者跑起来但功能太简陋。下面我会从整体设计到具体代码再到排错一步步拆开讲。1. 项目整体拆解与技术选型逻辑1.1 这套系统到底做了什么我最早接到这个需求的时候第一反应不是急着写代码而是先把系统的用户角色和核心流程想清楚。旅游景点推荐系统从用户角度看主要解决三件事找景点、看详情、收藏评分。从管理员角度看主要解决另外三件事维护景点信息、管理用户、分析数据。这几件事串起来就是一个完整的业务闭环。我最终做出来的系统功能清单大致是这样的前台用户端注册、登录、首页推荐列表、景点分类筛选、关键词搜索、景点详情页含图片轮播、标签、评分、评论列表、收藏功能、个人中心后台管理端景点信息 CRUD、分类管理、用户列表、评论审核、数据统计看板推荐逻辑基于热度和标签匹配的混合推荐登录用户会看到个性化推荐结果这套功能覆盖了景区类系统的常见需求做毕业设计足够有料放到简历上也能体现出完整的全栈能力。1.2 为什么是 Node.js Vue Express而不是别的组合很多人在技术选型时会纠结我直接说结论这个组合是当前中小型全栈项目里性价比最高的一套。先说后端Express 是 Node.js 生态里最成熟的 Web 框架中间件机制灵活路由写法直观特别适合接口服务。相比 Spring BootExpress 项目启动快、依赖少、上手成本低一个上午就能把基础架子搭起来。相比 Python 的 FlaskExpress 的 JS 全栈优势更明显——前后端同一种语言不用切换大脑模式。再说前端Vue 对新人非常友好。模板语法直观响应式数据绑定省掉大量手动 DOM 操作再加上 Element Plus 组件库后台管理页面基本就是拼积木。Vue 的生态在国内做得特别好遇到问题搜一下基本都有答案。数据库我用的 MySQL是因为这类系统的数据关系相对明确用户、景点、评论、收藏之间有关联查询需求关系型数据库最合适。如果要用 MongoDB 也可以但 MySQL 在面试时被问到频率更高而且写 SQL 的能力本身也是基本功。2. 核心模块与推荐算法设计2.1 功能模块怎么划分才会让代码不乱开始写代码之前强烈建议先把目录结构规划好。我的做法是把前后端分成两个独立目录互不干扰。后端按功能模块划分不是按文件类型划分这一点很重要。后端目录我用的这种结构server/ ├── app.js # Express入口中间件注册 ├── config/ │ └── db.js # 数据库连接配置 ├── routes/ # 按业务拆分的路由文件 │ ├── auth.js # 注册登录 │ ├── attractions.js # 景点相关 │ ├── categories.js # 分类相关 │ ├── favorites.js # 收藏相关 │ └── reviews.js # 评论相关 ├── controllers/ # 业务逻辑层 ├── middleware/ │ ├── auth.js # JWT鉴权中间件 │ └── upload.js # 图片上传 └── utils/ └── recommend.js # 推荐算法封装我见过太多人把所有接口都写在一个app.js里几十个路由堆在一起改一个需求要找半天。按模块拆分的好处是出了问题能快速定位面试官问起架构也能讲得清楚。前端我用 Vue CLI 创建项目用 vue-router 做路由用 axios 请求后端接口。页面组件按角色划分views/user放用户端页面views/admin放管理端页面公共组件放在components里。2.2 推荐算法不搞花架子用能落地的混合方案推荐系统是这个项目的灵魂但很多人一上来就想搞协同过滤、深度学习结果把自己搞死。我建议先明确一点对于毕设或中小型项目可解释、可维护的推荐逻辑远胜于花哨的模型。我用的方案是加权混合推荐综合三个维度计算景点得分热度分浏览量、收藏数、评论数越多权重越高标签匹配分用户历史行为涉及的景点标签与当前景点的标签重合度距离权重如果系统存了用户常驻城市可以给同城景点加分这个可选推荐分数的计算方式我简化成这样一个公式// 工具函数计算一个景点的综合推荐分 function calculateScore(attraction, userTags [], historyWeight 0) { const heatScore Math.log(attraction.views 1) 0.5 * Math.log(attraction.collections 1) 0.8 * Math.log(attraction.reviews 1); let tagScore 0; if (userTags.length 0 attraction.tags) { const intersection attraction.tags.filter(t userTags.includes(t)).length; tagScore intersection / Math.max(attraction.tags.length, 1); } // 0.6热度 0.4标签匹配historyWeight可调 return 0.6 * heatScore 0.4 * tagScore * 10 historyWeight; }说明一下Math.log是为了平滑长尾数据避免某个热门景点一骑绝尘。historyWeight用于在用户有收藏或浏览记录时给偏好类型的景点额外加分。获取用户偏好的时候我会去查用户最近的收藏记录和评论记录把景点标签拆出来统计出现次数最多的前三个标签作为userTags。这套逻辑跑起来效果不错而且容易写清楚答辩时能讲明白它为什么合理。2.3 数据库表设计两张表之间关系要能聊清楚这个项目的核心表一共六张每张表的职责非常明确表名主要字段作用usersid, username, password, nickname, avatar, city用户信息attractionsid, name, cover, images, summary, detail, tags, category_id, views, collections, reviews, address景点主表categoriesid, name景点分类favoritesid, user_id, attraction_id, create_time用户收藏关系表reviewsid, user_id, attraction_id, content, rating, create_time评论评分表browse_historyid, user_id, attraction_id, create_time浏览记录推荐算法用这里要特别提醒一个点不要把所有内容都塞进一个表里。比如景点图片因为一个景点有多张图我建议把图片存成 JSON 格式字符串或者单独建一张attraction_images表。毕设场景下用 JSON 字符串存images字段最简单查询的时候JSON.parse解析一下就行。外键我建议在应用层维护而不是数据库层面强制约束。原因很简单应用层维护更灵活删除景点时手动清理相关收藏、评论记录逻辑清晰也不容易出幺蛾子。3. 从零搭建完整实操过程3.1 环境准备最容易翻车的一关第一步是安装 Node.js。这里结合热搜词里最典型的问题说几个坑。去 Node.js 官网下载 LTS 版本我用的 18.x安装时一路 Next 就行。安装完打开终端验证node -v npm -v如果提示npm : 无法加载文件 ... npm.ps1因为在此系统上禁止运行脚本这是因为 Windows PowerShell 默认执行策略是 Restricted不允许运行 .ps1 脚本。解决方法很暴力也很简单用管理员身份打开 PowerShell执行一次Set-ExecutionPolicy -Scope CurrentUser RemoteSigned然后输A确认再重新打开终端npm 就正常了。还有人在安装 Node.js 时报错 2203这个错误一般是 Windows Installer 权限问题。解决方法是用管理员身份运行安装包或者在服务里确认 Windows Installer 服务已启动。实在不行就下免安装版解压后手动配环境变量这也是热搜里经常提到的“nodejs免安装环境配置”方案。3.2 后端构建Express 项目初始化和核心接口后端项目我习惯手动创建不用脚手架生成器这样每个文件的作用都心里有数。先建目录然后初始化mkdir server cd server npm init -y npm install express mysql2 cors jsonwebtoken bcryptjs multermysql2是 MySQL 的驱动支持 Promise比老牌的mysql包好用。bcryptjs用来给用户密码加密这里注意要用bcryptjs而不是bcrypt后者是原生模块Windows 下编译容易出幺蛾子。入口文件app.js的核心部分长这样const express require(express); const cors require(cors); const app express(); // 中间件注册顺序有讲究先cors再json不然POST请求会出问题 app.use(cors()); app.use(express.json()); app.use(/uploads, express.static(uploads)); // 路由汇总 app.use(/api/auth, require(./routes/auth)); app.use(/api/attractions, require(./routes/attractions)); app.use(/api/favorites, require(./routes/favorites)); app.use(/api/reviews, require(./routes/reviews)); app.listen(3000, () { console.log(Server running on http://localhost:3000); });数据库连接我单独放在config/db.jsconst mysql require(mysql2/promise); const pool mysql.createPool({ host: localhost, user: root, password: 123456, database: travel_db, waitForConnections: true, connectionLimit: 10, namedPlaceholders: true }); module.exports pool;创建数据库的时候记得把字符集设为utf8mb4不然存 emoji 或者生僻字会报错。景点列表接口和推荐列表接口是核心中的核心我详细说一下。景点列表支持分页和筛选推荐列表则是走utils/recommend.js里的算法。推荐接口的核心流程是先查用户偏好标签再查所有景点计算得分按得分降序返回前十条// routes/attractions.js 中推荐接口的简化逻辑 router.get(/recommend, authMiddleware, async (req, res) { const userId req.user.id; // 1. 查用户标签偏好 const userTags await getUserPreferenceTags(userId); // 2. 查全部景点 const [attractions] await pool.query(SELECT * FROM attractions WHERE status 1); // 3. 遍历计算推荐分 const scored attractions.map(item { const score calculateScore(item, userTags); return { ...item, score }; }); // 4. 倒序取前10 scored.sort((a, b) b.score - a.score); res.json({ code: 0, data: scored.slice(0, 10) }); });3.3 前端构建Vue 项目初始化和关键页面前端用 Vue CLI 创建项目npm install -g vue/cli vue create web cd web npm install axios vue-router4 element-plus这里有个小坑Vue CLI 创建项目时会让你选预设建议选Manually select features然后勾选 Router这样会自动装好 vue-router。版本上Vue 3 项目对应 vue-router 4Vue 2 项目对应 vue-router 3别搞混了。Element Plus 是完整的 UI 组件库登录页、表单、表格、弹窗都能直接用它省事很多。按需引入可以去官网看插件但我图省事直接全量引入毕设场景完全够用import { createApp } from vue; import ElementPlus from element-plus; import element-plus/dist/index.css; const app createApp(App); app.use(ElementPlus);前端的核心页面有三个首页、景点详情页、后台管理页。首页是推荐系统的脸面我用了一个 banner 轮播加推荐列表。推荐列表的数据来自/api/attractions/recommend接口。要考虑用户可能没登录所以前端要先判断 token 存不存在没登录就调普通列表接口登录了才调推荐接口。这里我封装了一个request.js统一处理 axios 的 baseURL 和 token 注入// request.js 中请求拦截器每次请求自动带上token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; });景点详情页需要处理路由参数从列表页跳转过来带上景点 id// 列表页跳转 router.push({ path: /detail, query: { id: item.id } }); // 详情页读取参数 const route useRoute(); const id route.query.id;这里要用query而不是params因为params在页面刷新后会丢失query的参数会保留在 URL 上刷新也不怕。这是新人特别容易踩的坑。3.4 前后端联调接口通了才算真的跑起来前端开发服务器默认跑在 8080 端口后端跑在 3000 端口直接请求会出现跨域问题。虽然后端加了cors()但本地开发时更推荐用 Vue 的代理功能在项目根目录下创建vue.config.jsmodule.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } };这样前端请求/api/xxx时开发服务器会自动转发到后端的 3000 端口。比在后端开 CORS 更干净因为打包上线后nginx 也是用类似的反向代理方式处理。联调阶段我的习惯是先把后端接口用 Postman 测一遍确认返回结构没问题再开始写前端页面。不然前后端一起调试出了问题都不知道是哪一端。4. 常见问题与排查我踩过的坑都在这4.1 环境配置类问题先说 npm 脚本禁止运行的问题前面已经提过解决方案这是 Windows 用户最常遇见的拦路虎。还有一个是高版本 Node.js 装旧包报错比如提示node-gyp编译失败优先升级或重装依赖不要硬刚。还有一个是数据库连接数耗尽报错在写好连接池的情况下基本不会出现但如果你用mysql.createConnection而不是连接池并发稍高就会报Too many connections。解决方案就是改成连接池也就是前面config/db.js里的写法。4.2 后端权限和时间格式问题接口权限是最容易漏的。很多人的系统里未登录用户直接调后台管理的删除接口也能成功这就是没做鉴权。我的做法是在需要登录的路由上挂authMiddleware在管理员接口上再挂一层isAdmin中间件判断用户角色。时间格式问题也很恶心。MySQL 里的DATETIME通过 JSON 返回给前端时会变成2025-01-12T08:30:00.000Z这种格式直接显示很难看。解决方案是在返回前统一格式化function formatTime(date) { if (!date) return ; const d new Date(date); return ${d.getFullYear()}-${d.getMonth()1}-${d.getDate()}; }4.3 vue-router 的 history 模式与打包异常Vue 项目打包后如果刷新页面出现 404十有八九是路由模式问题。Vue Router 默认是hash模式URL 里带个#不会向服务器发请求所以不会404。一旦改成history模式就需要服务器端配置 fallback把所有请求重定向到 index.html。如果你用的是 Express 托管前端静态文件需要加一行app.get(*, (req, res) { res.sendFile(path.join(__dirname, ../web/dist/index.html)); });不然刷新就 404。这是面试官最喜欢问的考点之一能讲清楚说明你确实发布过项目。还有“vue 打包后布局异常”这个热搜词很多情况是 CSS 和图片路径问题。生产环境的静态资源应放在绝对路径下在vue.config.js里设置publicPath: ./或部署到域名根目录不然 CSS、JS 加载不到页面就变得跟裸奔一样布局全乱。4.4 vue 播放 m3u8 的问题景点详情页有时候要放宣传视频如果用 m3u8 流媒体格式原生video标签是播不了的需要借助 hls.js。加载m3u8的核心代码如下import Hls from hls.js; function playM3u8(videoEl, url) { if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(url); hls.attachMedia(videoEl); } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src url; // Safari直接支持 } }CDN 播放时容易碰到跨域问题比较快的解决思路是让后端把 m3u8 和 ts 分片的响应头加上Access-Control-Allow-Origin或者前端用proxy代理到视频服务器。4.5 其他杂项坑图片上传后如果在前端显示不出来先检查路径返回的是相对路径还是绝对路径再看 Express 是否注册了express.static(uploads)。如果上传到服务器的图片在新页面能看到、列表页看不到多半是缩略图没刷新需要加时间戳参数让浏览器绕过缓存。Express 路由参数中包含中文比如搜索景点关键词前端encodeURIComponent编码一次后端解析回来就是正常的。如果中间还有一层代理可能要多编一次这个问题能排查半天。5. 项目还能怎么扩展现在这个系统已经能完整跑通但如果你想让它更有亮点、答辩时更有话说可以在几个方向上做扩展对接地图服务在景点详情页展示地图定位和周边推荐。腾讯地图的 JS SDK 有专门的 Vue 组件申请 key 后就能用工作量不大但视觉效果好。增加门票预订功能。可以做简单的票务模块生成订单号模拟支付流程。热搜里有“express微信支付”如果真要接真实支付需要商户号和证书流程很长我建议做个模拟支付的开关演示时走假支付流程但代码里预留好真实支付的接口位置。这个设计在答辩时能体现工程思维。管理后台加数据可视化。用 ECharts 展示景点热度排行、用户增长曲线、各分类占比等数据量不用大演示效果足够好。这类可视化图表本身就是加分项。关于推荐算法可以更进一步。现在的标签匹配方案属于基于内容的推荐想要更高级的效果可以再叠加一个“收藏了某个景点的人还收藏了什么”的逻辑这就是最简版的协同过滤。两个方案的结果按比例融合答辩时能把这个演进过程讲清楚是很大的亮点。最后再分享一个我做这类全栈项目最深切的体会先把最小的闭环跑通再迭代功能。不要一上来就想着把推荐算法、后台管理、图片上传全部做完而是先用最朴素的 CRUD 接口加上简单的列表页让数据从前端到后端到数据库完整流转起来这个“骨架”通了之后后面加功能都是往骨架上长肉很快也不容易出大问题。如果你正在做类似的系统按照上面这套方案按部就班推进一两个星期做到能演示的完整版本完全没问题。中途卡住的时候优先去查日志——后端控制台的打印、浏览器的 Network 面板、数据库的报错信息这三个位置能解决九成的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价