news 2026/10/13 10:35:03

SpringBoot+Vue前后端分离文旅网站全栈开发与Nginx部署实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue前后端分离文旅网站全栈开发与Nginx部署实战

做文旅网站这个选题,在毕设和个人项目里一直挺常见。但“七彩云南文化旅游网站”这类项目有个特点:名字听着像内容站,实际上踩的是完整的全栈开发链路——从表结构设计到接口鉴权,从组件拆分到Nginx部署,每一环都会暴露问题。我最近帮一个学弟完整调通了这套前后端分离的系统,技术栈是SpringBoot + Vue + MyBatis + MySQL,顺手复盘一下整个开发和部署过程中真正有价值的细节,希望能帮你少走点弯路。

1. 项目定位与技术选型:为什么这套组合成了文旅网站的主流方案

1.1 先想清楚:这个系统解决的是什么问题

文化旅游网站表面上是“展示景点 + 发布攻略”,但剥开来看,它覆盖了一套标准的信息管理场景:游客要查景点、看详情、收藏感兴趣的路线;管理员要维护景点信息、管理评论、发布推荐内容。这意味着系统必须同时具备面向C端的展示能力,和面向B端的后台管理能力。

前后端分离的价值就在这里体现出来了。传统JSP直接渲染页面,首页和后厨揉在一起,改一个展位样式可能要动整个工程。而用SpringBoot做纯后端接口,Vue单独维护页面,两端通过JSON通信,发布和迭代互不干扰。对于一个人单打独斗做毕设或作品集的场景,这种结构也更容易分阶段开发——先把后端接口跑通,再写前端页面,最后联调,不至于一开始就卡在某个环节动弹不得。

1.2 这套技术栈的取舍逻辑

SpringBoot为主流选择,原因不用多说:配置简化、生态成熟、招工时面试官认可度也高。MyBatis这个点反而是不少同学容易纠结的地方——既然SpringData JPA也很好用,为什么选MyBatis?

我的判断是,文旅网站这类业务查询条件多、动态SQL频繁(按地区筛选、按分类筛选、按关键词搜索、标签组合查询),MyBatis对SQL的可控性最强。写一个动态<where>标签就能搞定多种条件组合,SQL调优时也能直接看到最终执行的语句。相比JPA的复杂查询,MyBatis的学习成本更集中在XML语法上,一旦熟练了,写业务接口的效率非常高。

前端选择了Vue,而不选React或Angular,核心原因是上手曲线平滑,中文资料多,遇到问题随手能搜到答案。Vue的响应式数据绑定在做“收藏列表”“评论展示”这类频繁交互的场景里非常顺手,v-for渲染列表、v-if控制权限按钮、computed计算收藏状态,都是直观的写法。

MySQL作为数据库则完全没有争议,免费、稳定、应用广泛,配合Navicat或命令行工具就能完成建库、导数据、调试。

提示:如果你看到市面上很多同类项目用MyBatis-Plus,也不用纠结。这个项目用纯MyBatis,锻炼的是手写SQL的能力;MyBatis-Plus只是在此基础上简化CRUD。学写过一遍手写SQL,再用任何ORM框架都轻松。

2. 前后端分离的模块规划与数据库设计

2.1 功能模块怎么拆

我建议先按角色切分:普通游客和管理员。游客能看到网站展示面,管理员能进入后台维护数据。基于这个思路,前端拆成两个端——门户端和管理端;后端则统一对外暴露接口,按模块组织Controller。

门户端的核心模块通常是:

  • 首页:轮播图、热门景点推荐、精选旅游资讯
  • 景点模块:景点列表(含筛选分页)、景点详情(含图集和简介)
  • 攻略/资讯模块:旅游攻略文章,支持分类查看
  • 用户模块:注册、登录、个人中心、我的收藏
  • 互动模块:评论景点、收藏景点

管理端的核心模块则是:

  • 管理员登录
  • 景点管理:增删改查、上下架
  • 资讯管理:发布和编辑文章
  • 评论管理:审核和删除
  • 用户管理:查看注册用户列表

一个常见的错误是功能越加越多,最后做出一堆没用的页面。文旅网站的核心价值在于“信息展示 + 用户互动”,先把这两个主链路打通,再考虑扩展。

2.2 核心表结构设计

数据库设计是整个项目的地基。我建议建这几张核心表,字段不必多,但要满足业务闭环:

表名用途关键字段
user用户表id、username、password(加密)、nickname、avatar、role、create_time
category景点分类表id、name、sort
attraction景点表id、name、intro、content、image、category_id、region、status、view_count、create_time
article资讯/攻略表id、title、content、cover、category、author、create_time
comment评论表id、user_id、attraction_id、content、create_time、status
favorite收藏表id、user_id、attraction_id、create_time

其中有几个字段值得单独说说。

一是password必须存加密后的密文,不能明文入库。常见做法是用MD5加盐或BCrypt。这个项目里我用BCrypt,因为Spring Security里可以直接用BCryptPasswordEncoder,但如果你没引入Spring Security,也可以单独引入spring-security-crypto依赖,只取加密工具类。

二是景点表里的status字段,用于上架/下架。很多同学不做这个字段,导致删景点只能物理删除,数据一旦被误删就找不回来了。加了状态字段后,后台“下架”其实就是把状态改成0,用户端查询条件带status = 1,实现简单又安全。

三是region字段,用于按地区筛选。云南文旅的典型场景就是“按目的地找景点”,所以这个字段在筛选查询中会频繁用到,建议建索引。

CREATE TABLE attraction ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, intro VARCHAR(500), content TEXT, image VARCHAR(255), category_id INT, region VARCHAR(50), status TINYINT DEFAULT 1, view_count INT DEFAULT 0, create_time DATETIME );

2.3 联表查询的设计意图

MyBatis联表查询通常有两种实现思路:resultMap嵌套查询和分步查询。这个项目里,我推荐一个直接但实用的做法:写一个AttractionVO,包含景点基本信息加上分类名称,用一条LEFT JOIN查出来。

<select id="selectAttractionVOList" resultType="com.xxx.vo.AttractionVO"> SELECT a.*, c.name AS categoryName FROM attraction a LEFT JOIN category c ON a.category_id = c.id <where> <if test="region != null and region != ''"> AND a.region = #{region} </if> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (a.name LIKE CONCAT('%', #{keyword}, '%') OR a.intro LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY a.view_count DESC </select>

这种写法在数据量不大时性能完全够用,同时还能让前端一次性拿到展示所需的数据,不用额外再调一次分类查询接口。

3. 后端实现要点:鉴权、检索、评论三个核心功能

3.1 登录注册与登录状态保持

登录鉴权是几乎所有全栈项目的入口,也是新手最容易做得“假”的地方。不少同学做登录只是校验一下用户名密码,然后跳转页面,刷新后状态就丢了。这种做法在前后端分离项目里是行不通的,因为后端接口无状态,必须有一种机制让服务器识别“当前请求来自谁”。

稳定的做法是采用JWT(JSON Web Token)方案?用户登录成功后,后端生成一个Token返回给前端,前端把它存在本地,后续每次请求都在请求头里带上Authorization: Bearer <token>。后端通过拦截器统一校验Token,从Token中解析出用户ID等信息。

流程拆开是三步:

  1. 登录接口校验用户名密码,成功后用io.jsonwebtoken.JJWT生成Token,把用户ID、用户名、角色塞进去,设置过期时间(一般2小时)。
  2. 写一个拦截器LoginInterceptor,实现HandlerInterceptor接口,在preHandle里校验Token,校验通过就把用户信息放入request。
  3. 在WebMvcConfigurer里注册拦截器,并配置放行路径——登录、注册、首页、景点列表这些公开接口要放行,收藏、评论、后台管理接口要拦截。
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); if (jwtUtil.validateToken(token)) { Integer userId = jwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; } } response.setStatus(401); return false; } }

这里有个细节值得强调:拦截器里校验通过后,一定要把userId放进request里,后续Controller的方法参数上加@RequestAttribute("userId") Integer userId就能拿到当前登录用户,不需要再查一遍数据库。这是很实用的性能优化习惯。

注册接口稍微简单点——检查用户名是否重复,加密密码,然后插入user表。这里建议在创建用户时直接给定role为普通用户,管理员账号可以手动在数据库里插一条,或者预留一个固定账号初始化,避免把“创建管理员”的逻辑暴露成普通接口。

3.2 景点筛选与分页查询的落地写法

景点列表页几乎必然要用到“按分类筛选 + 按地区筛选 + 按关键词搜索 + 分页”。在前后端分离模式下,这些条件都应该通过Query参数传递,后端用一个统一的分页参数接收。

分页实现有两种路径:MyBatis手写LIMIT #{offset}, #{pageSize},或者用PageHelper插件。我建议新手第一版手写分页,多写几行代码但能把原理吃透——offset = (page - 1) * pageSize,然后查总数时候SELECT COUNT(*)。把这些逻辑封装到一个简单的PageResult<T>类里,包含total、list、pageNum、pageSize四个字段,前端就能完整展示分页组件。

不推荐用PageHelper的原因在于:它通过拦截器自动改写SQL,偶尔会出现统计查询和多表查询的边界case,出了问题不好排查,而且依赖一旦不维护了,项目的升级成本会变高。

接口的Controller层建议保持“薄”的状态,只做参数接收和结果封装,具体业务逻辑放到Service层。这样一个Controller对应一组接口,后面管理端和门户端共用一套Service,减少重复代码。

3.3 评论、收藏模块的实现思路

评论模块的业务逻辑很清晰:用户提交评论(内容校验)、保存进评论表、景点详情页按时间倒序展示评论列表。但这里有一层“审核”维度容易被忽略——评论表里要有status字段,默认待审核。展示接口只查status = 1(已通过)的评论,管理端可以查看全部评论并修改状态。不用做太复杂的审核流,但这个字段一定要有,否则评论区被灌垃圾内容时你没有兜底手段。

收藏模块要考虑的是“幂等”问题。用户连续点击两次收藏按钮,后端不能插入两条收藏记录。规范做法是给favorite表加一个唯一索引(user_id, attraction_id),插入前先查一次是否已存在;收藏和取消收藏两个接口可以合并成一个“切换收藏”逻辑:查询到记录就删除,查不到就插入。

ALTER TABLE favorite ADD UNIQUE INDEX uk_user_attr (user_id, attraction_id);

个人中心展示“我的收藏列表”时,用收藏表关联景点表查出来即可。前端显示的是风景卡片列表,数据源来自这个关联查询。

4. 前端Vue实现:从页面拆分到接口联调的完整过程

4.1 页面与路由规划

Vue项目我用Vue CLI搭建。页面结构推荐按“两个端”组织:

src/ views/ portal/ # 门户端 Home.vue AttractionList.vue AttractionDetail.vue ArticleDetail.vue Login.vue Register.vue Profile.vue admin/ # 管理端 AdminLogin.vue AttractionManage.vue ArticleManage.vue CommentManage.vue router/ index.js api/ request.js attraction.js user.js

路由层面要有两个重点。一是用vue-router的懒加载(component: () => import(...)),这样首屏只加载需要的页面,不用把所有组件的JS都下载下来。二是对管理端路由做守卫——beforeEach里判断本地Token和用户角色,普通用户访问管理路由时直接重定向到首页。

4.2 axios封装与接口对接

前后端分离项目里的第一个大坑就是接口调用。我习惯在api目录下统一封装axios实例,设置基础URL和请求拦截器:

import axios from 'axios' const request = axios.create({ baseURL: 'http://localhost:8080', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default request

统一封装的好处很明显:每个页面不需要重复写Token逻辑和错误处理。后续如果接口地址变了,只需要改一个文件。页面里直接调用:

import { getAttractionList } from '@/api/attraction' getAttractionList({ page: 1, pageSize: 8, region: '滇西北' }).then(res => { this.attractions = res.list this.total = res.total })

这里有个容易搞混的点:如果SpringBoot后端配置了server.servlet.context-path=/api,那么baseURL应该写http://localhost:8080/api。前后端联调最头疼的问题就是路径对不上,建议一开始就统一接口前缀,比如所有控制器都加上/api前缀,这样规范又省事。

4.3 组件交互细节

景点详情页是互动功能最密集的页面,包括景点图片展示、介绍文本、评论列表、收藏按钮。收藏按钮的状态需要根据“当前用户是否已收藏该景点”来决定。做法是:加载详情的同时调一个“查询是否已收藏”的接口(如果用户已登录),返回true或false,按钮样式随之切换。

评论区建议做成“登录后才能评论”的逻辑。前端在提交按钮上判断本地是否存在Token,没有Token就弹窗提示跳转登录页;后端拦截器也做同样的校验,双保险。评论列表的渲染很简单,v-for遍历即可,难点在于评论时间和日期的格式化。后端返回的时间格式一般是2025-01-15T10:30:00带T的字符串,前端需要处理一下,用JavaScript的Date对象或者一个小函数把它格式化成2025-01-15 10:30展示。

5. 本地运行与服务器部署的完整链路

5.1 本地开发环境的搭建

环境准备这一步看似基础,但很多人卡在版本不匹配上。我推荐的版本组合是:JDK 1.8、Maven 3.6+、MySQL 5.7或8.0、Node 14+、Vue CLI 4.x。如果JDK版本和SpringBoot版本对不上,启动时会有一堆莫名其妙的依赖报错。

数据库初始化建议写一个init.sql脚本,把建库、建表、初始数据都放进去。初始数据很关键——首页一打开就要有景点展示,如果数据库是空的,前端页面显得特别难看。可以插入10到20条景点样本数据和5条资讯示例数据,图片地址先用网络占位图或者项目内的静态资源地址,后面再替换。

导入数据时如果遇到中文乱码,多半是MySQL客户端连接字符集和表字符集不一致导致的。建库时明确指定utf8mb4,并在application.yml里把连接参数的characterEncoding设为utf-8。

5.2 后端打包与前端构建

本地开发跑通联调后,就要准备部署。后端打jar包的命令很简单:

mvn clean package -DskipTests

打包时两个细节一定要检查。一是application.yml里的数据库账号密码和服务器环境保持一致;二是本地如果用了文件上传功能,上传目录的路径最好配置成可配置的,不要写死成C:/xxx,否则换到Linux服务器就崩。

前端构建命令:

npm run build

构建完成后会在dist目录生成静态文件。这里需要把vue.config.js里的publicPath设置成'/',否则部署到服务器后,页面加载的JS、CSS路径会出现相对路径错乱,首页能打开但样式全丢。

5.3 Nginx反向代理与静态资源托管

服务器上最省心的部署方案是:后端jar包用nohup常驻运行,前端dist目录交给Nginx托管,Nginx再把/api路径的请求反向代理到后端的127.0.0.1:8080。

Nginx配置核心部分:

server { listen 80; server_name your_domain; location / { root /www/wwwroot/travel/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; } }

try_files $uri $uri/ /index.html;这行特别重要,它能解决Vue Router的history模式刷新404问题。如果不写这行,用户刷新页面时Nginx会去磁盘找不存在的文件路径,返回404;加了这行,所有前端路由都会回退到index.html,由前端路由接管。

后端jar包的使用方式:

nohup java -jar travel-app.jar --server.port=8080 > app.log 2>&1 &

后端日志写进app.log,出问题直接tail -f app.log查看,比在控制台干瞪眼高效得多。

6. 开发部署中踩过的坑与排查思路

6.1 跨域问题:现象最吓人,本质最简单

前后端分离开发时,前端跑在http://localhost:8081,后端跑在http://localhost:8080,浏览器会拦截跨域请求。遇到的典型报错是:No 'Access-Control-Allow-Origin' header is present,页面看起来像是接口挂了,但后端日志里接口明明被调用了。

排查链路是这样走的:先用Postman直接调用后端接口,发现能正常返回数据,说明后端逻辑没问题。那么问题定位在前端到后端的请求链路被浏览器策略拦截了,也就是跨域。

解决办法两种:一种是给前后端配上统一域名端口,这需要改前端打包配置;另一种更常用——后端开启CORS支持。SpringBoot里写一个配置类:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

本地开发最简单的方式其实是配置Vue的devServer代理,把/api代理到后端地址,这样浏览器看到的是同源请求,完全绕开跨域:

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

6.2 日期格式返回带T的问题

开发评论列表时,前端发现时间字段显示成2025-01-15T10:30:00.000+00:00,看着很别扭。根因是Jackson默认序列化Java时间类型时的格式不规范。

排查过程:先看数据库里存的时间对不对——没问题;再用Postman调用详情接口——发现返回的JSON里时间带T。问题定位到后端JSON序列化配置。

解决办法是在application.yml里配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

time-zone这行非常关键,不加GMT+8的话,存进数据库的时间跟北京时间会有8小时时差。

6.3 图片上传后前端访问不到

景点管理功能里,管理员需要上传景点图片。我第一版只在后端Controller里接收MultipartFile并保存到本地目录,结果上传成功后图片URL返回的是http://localhost:8080/upload/xxx.jpg,但浏览器访问这个地址直接404。

排查思路:先确认保存文件的目录下有没有这个文件——有;再确认后端有没有静态资源映射——没有。问题根因是SpringBoot默认只映射classpath:/static/下的静态资源,自定义的上传目录不在映射范围内。

解决方案是加一个WebMvc配置,把/upload/**映射到磁盘目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir + "/"); }

服务器部署时,uploadDir要改成Linux下的绝对路径,例如/www/wwwroot/upload/,并且注意目录权限,否则保存文件时会报权限不足。

6.4 数据库初始化与SQL版本兼容

同学在本地MySQL 8.0导入SQL时遇到一个报错:Unknown collation: utf8mb4_0900_ai_ci。这个排序规则是MySQL 8.0的新特性,如果用很老版本的数据库工具导入,就会报错。

排查过程是:检查SQL文件里建表语句的字符集写法,发现使用了MySQL 8.0默认的排序规则;再确认本地数据库版本,发现是8.0,理论上不会冲突,但导入工具解析出错。

解决办法是调整SQL文件里的字符集配置,统一改成utf8mb4_general_ci,这个排序规则在各个版本都能识别。这类问题在数据库版本差异上很常见,建议写初始化SQL时尽量用通用性强的配置。

最后想说的

做这个项目,我最深的体会是:八成的时间不是花在写功能上,而是花在解决环境、配置、边界条件上。你可能会因为一个跨域问题折腾一个小时,然后发现只是少加了一行注解;也可能会因为数据库时区配置不对,整个项目的时间全部错乱。这些调试过程看起来琐碎,但恰恰是它们真正锻炼了一个开发者排查问题的能力。

如果你要用这套代码做毕业设计或作品集,我的建议是:先耐着性子把数据库表设计完整,把初始数据准备好,再写后端接口,最后做前端页面。顺序一定不要颠倒。表结构不稳,后面所有接口和页面都要跟着返工。另外,部署到服务器后一定自己完整走一遍“游客浏览—注册登录—收藏—管理员登录—修改景点信息”的主流程,确认没有遗漏,再做演示。文旅网站这类项目,功能不用贪多,主链路完整流畅,答辩时的说服力远比堆砌十几个半成品功能要强得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 10:34:59

基于Node.js和Vue的养老院管理系统开发实战

这套养老院管理系统我从零完整跑通了一遍&#xff0c;从Node.js后端接口到Vue前端页面&#xff0c;再到数据库设计和部署联调&#xff0c;中间踩了不少坑&#xff0c;也积累了不少实打实的经验。如果你正准备做类似的Web项目&#xff0c;或者是拿这个题目当毕业设计&#xff0c…

作者头像 李华
网站建设 2026/10/13 10:34:54

字节跳动TRAE整合TraeWork停服:开发者迁移指南与数据备份要点

字节跳动的 AI 编程工具 TRAE 最近做了一次产品线整合。根据公开信息&#xff0c;TRAE 将合并两款 AI 工具&#xff0c;字节旗下的 TraeWork 将于 10 月 23 日停止服务。对还在使用 TraeWork 的开发者来说&#xff0c;这不是一条可以划走的新闻&#xff1a;停服意味着历史项目资…

作者头像 李华
网站建设 2026/10/13 10:34:48

.NET Core WebSocket实战:服务端接入、客户端重连与排坑指南

WebSocket在服务端开发里早就不是什么新鲜东西&#xff0c;但每次写.NET Core项目&#xff0c;总有人来问我这套到底怎么接、怎么用才不出幺蛾子。这篇我把实际项目中WebSocket从服务端到客户端的完整用法、参数配置、踩坑经验一次讲清楚&#xff0c;不绕弯子&#xff0c;直接能…

作者头像 李华
网站建设 2026/10/13 10:34:47

Servlet+JSP打造非遗电商:织金砂锅推广平台项目实战解析

1. 织金砂锅推广平台到底在推广什么&#xff1a;需求分析先于代码 很多人一看到"Servlet 家乡特产 推广平台"的组合&#xff0c;第一反应是&#xff1a;这不就是一个课设级别的Java Web项目吗&#xff1f;能有什么含金量&#xff1f;说实话&#xff0c;我一开始也是…

作者头像 李华
网站建设 2026/10/13 10:34:19

DeepSeek职场应用指南:从提示词模板到工作流实操

简介&#xff1a;这份PDF资料是清华大学DeepSeek系列讲座第二讲&#xff0c;共35页&#xff0c;面向希望将大模型落地于职场场景的技术人员、产品经理与管理者。内容围绕DeepSeek职场赋能框架展开&#xff0c;涵盖Organization、Innovator、Reasoner、Chatbot四类角色定位&…

作者头像 李华
网站建设 2026/10/13 10:34:18

访问控制列表ACL详解:作用、分类、通配符与配置实战

做网络运维或者后端开发的同学&#xff0c;大概率都见过“ACL”这个词。ACL 的全称是 Access Control List&#xff0c;中文一般叫访问控制列表。它看起来只是设备上的一段规则集合&#xff0c;但在整个网络安全体系中&#xff0c;它的位置非常基础又非常关键。无论是企业路由器…

作者头像 李华