ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue前后端分离旅游网站实战:从数据库到部署

SpringBoot+Vue前后端分离旅游网站实战:从数据库到部署 最近刚把一个前后端分离的旅游网站系统完整跑通从数据库建表到后端接口开发再到前端页面联调最后部署上线整个链路都走了一遍。这套系统用的是SpringBootVueMyBatisMySQL这套经典组合前端负责页面渲染和用户交互后端只提供JSON接口两者完全解耦。如果你正在准备毕业设计、课程设计或者想找个完整的练手项目吃透前后端分离开发流程这篇文章可以帮你省掉大量摸索时间。我先把这套系统的核心价值说清楚它不是那种只放几个静态页面的演示项目而是包含了用户注册登录、景点展示、搜索筛选、酒店预订、订单管理、评论收藏、后台管理等完整业务闭环的真实系统。所以无论你是想学习技术栈整合还是需要一套能直接交差的源码这套系统的设计思路和踩坑经验都值得过一遍。下面我会按项目结构、数据库设计、后端实现、前端实现、部署联调这条主线把关键细节和实操中容易翻车的地方全部摊开讲。1. 项目整体设计与技术选型思路1.1 为什么选用前后端分离架构很多初学者第一次做项目时会纠结直接用Thymeleaf模板引擎把页面和后端写在一起不是更简单吗为什么非要搞前后端分离我最初也这么想过但真正上手后发现前后端分离带来的好处是单体应用没法比的。首先是开发职责清晰前端只管Vue组件、路由、状态管理后端只关心接口逻辑和数据持久化两边可以并行推进不需要互相等。其次是部署灵活前端构建出来的纯静态文件扔到Nginx就能跑后端打成jar包独立启动任何一个挂了都不影响另一个维护成本低很多。还有一个很多人忽略的点接口复用。旅游网站这种业务用户端和管理端共用同一套后端接口很常见。如果是单体模板项目管理后台想复用用户端的查询逻辑就得重新写页面模板而前后端分离架构下管理端就是一个独立Vue应用直接调用同一套API就行。这个优势在项目后期扩展时特别明显。当然前后端分离也有代价最直接的就是跨域问题和Token鉴权问题这个后面我会专门讲怎么处理。但总体来看对于旅游网站这种需要频繁迭代、多端复用的业务场景前后端分离是更合理的选择。1.2 技术栈选型详解这套系统的技术栈选择很有代表性基本都是Java全栈开发中的“标配”我逐个说下选型理由和需要注意的点。后端SpringBootSpringBoot的核心价值就是简化配置。以前用SpringMVC写个项目光XML配置就能写几百行SpringBoot用自动配置约定大于配置把大部分样板代码都省掉了。内置Tomcat也让部署变得非常简单直接java -jar就能启动。不过要注意版本问题SpringBoot 2.x和3.x差异比较大3.x要求JDK17而且部分第三方依赖兼容性还不完善。这套系统用的SpringBoot 2.7.x搭配JDK8/11都能跑兼容性最稳适合学习阶段使用。持久层MyBatisMyBatis相比JPA/Hibernate最大的优势是SQL完全由自己控制复杂查询优化起来非常方便。旅游网站的搜索条件往往是动态组合的比如按价格区间、按评分、按目的地筛选这种场景用MyBatis动态SQL处理非常顺手。而且MyBatis学习曲线平缓SQL写得好的人基本半小时就能上手。前端VueVue的上手门槛比React低模板语法直观对新手友好。这套系统用的Vue2 Vue Router Vuex ElementUI组合ElementUI的后台管理组件开箱即用开发效率极高。如果你之前没接触过Vue先去把指令、组件通信、生命周期这三块弄明白剩下的边写边查就行。数据库MySQLMySQL就不用多说了开源免费、生态成熟、资料多是中小型项目的首选。需要注意版本和字符集配置5.7以上版本默认字符集是utf8mb4能完整支持中文和生僻字千万别用老版本的utf8不然存emoji或特殊字符会报错。下面我用个表格把技术栈和版本选型列出来方便你直接参照技术组件选型版本说明JDK8 / 11稳定版本适配SpringBoot 2.xSpringBoot2.7.x内置Tomcat打包为可执行jarMyBatis2.2.xSpringBoot starter需要配合mybatis-spring-boot-starterMySQL5.7 / 8.0字符集务必设为utf8mb4Vue2.6.x配套Vue Router 3.x、Vuex 3.xUI组件库ElementUI 2.15.x表格、表单、弹窗等组件齐全构建工具Maven 3.6 / npm 6分别管理后端和前端依赖1.3 功能模块与页面结构梳理在动手写代码之前把功能模块梳理清楚非常重要否则开发过程中很容易想起一出是一出搞得代码结构混乱。旅游网站系统我按用户角色拆分成三个端用户端面向普通游客首页展示推荐景点和轮播图景点列表支持分类筛选和关键词搜索景点详情页展示图文介绍和视频宣传酒店预订模块支持按城市查询和下单个人中心包含订单查询、收藏管理、个人资料修改。核心操作路径是浏览景点 - 查看详情 - 收藏/预订 - 订单管理。管理端面向管理员景点管理增删改查和上下架、酒店管理、订单管理查看所有用户的订单、修改订单状态、用户管理查看用户列表、禁用账号、轮播图管理。管理端和后端服务器通常部署在同一台机器上访问路径和用户端分开。公共模块用户注册登录、JWT Token鉴权、图片上传、统一异常处理。页面结构上用户端主要包含首页、景点列表页、景点详情页、酒店列表页、酒店详情页、订单确认页、个人中心管理端则是登录页、后台布局框架侧边栏顶栏、各管理页面。这样拆完之后前后端接口的数量和字段关系基本就心里有数了。2. 数据库设计与核心表结构详解2.1 六大核心表的设计思路数据库设计是整套系统的地基表结构一旦定下来后面改起来成本很高所以一定要提前想清楚。旅游网站系统的数据库我拆成了六张核心表用户表、景点表、酒店表、订单表、评论表、收藏表另外再加一张轮播图表用于首页展示。用户表user字段包括id、username、password、nickname、avatar、phone、email、role角色区分普通用户和管理员、status账号状态是否被禁用、create_time。密码绝对不能明文存储要用MD5加盐或BCrypt加密。这里的role字段为后续扩展管理员功能预留了空间默认注册用户都是普通角色。景点表scenic字段包括id、name、description、cover封面图URL、images图集用JSON字符串或逗号分隔存储多个图片地址、video_url宣传视频地址、category景点分类比如自然风光、历史古迹、province、city、address、price门票价格、open_time、recommend是否推荐到首页、status上下架状态、view_count浏览量、create_time。这里比较关键的是分类和城市字段它们在列表页筛选时会频繁使用必须加索引。酒店表hotel字段包括id、name、cover、images、description、province、city、address、price起步价、score评分、facility设施服务JSON数组存储、status、create_time。酒店表和景点表结构上有一定相似性这也是很正常的因为它们展示逻辑类似只是业务字段不同。订单表orders字段包括id、order_no订单编号唯一、user_id、scenic_id关联景区门票订单时或hotel_id关联酒店订单时、type订单类型门票/酒店、quantity数量、amount总金额、contact_name联系人、contact_phone、book_date出行日期或入住日期、status订单状态待支付/已支付/已取消/已完成、create_time。订单表是业务的核心user_id和scenic_id/hotel_id都要建索引因为查询订单列表时总是按用户或按景点/酒店维度去查。评论表comment字段为id、user_id、scenic_id或hotel_id、content、score、create_time。评论和订单是两个独立模块评论时需要通过用户ID和景点/酒店ID关联校验该用户是否购买过避免刷评论。收藏表favorite字段为id、user_id、scenic_id、create_time。这里有一个设计选择为什么用单独的表而不是在用户表里加一个favorite_ids字段因为用户收藏的景点数量不确定用关联表既方便查询当前用户收藏了哪些景点又能方便统计每个景点被多少人收藏。2.2 字段类型与索引设计经验刚开始做数据库设计时我经常凭感觉定字段类型后来发现这么做坑很多。这里分享几条实际开发中总结出来的经验。首先是金额字段。价格千万不要用float或double因为浮点数在比较时会出现精度问题比如100.1实际存储可能是100.0999999。应该用decimal(10, 2)专为精确计算设计。其次是文本字段。景点介绍、酒店描述这种内容可长可短用VARCHAR(255)很容易不够用用TEXT又担心浪费空间。我的做法是可能超过255个字的用TEXT其余统一用VARCHAR。MySQL的TEXT类型最多能存65535字节对景点介绍和酒店描述来说完全够用。再就是时间字段。统一用DATETIME不要用TIMESTAMP。原因在于TIMESTAMP有2038年问题虽然离我们还远但既然DATETIME没有这个限制直接用更省心。插入时用NOW()或MyBatis的CURRENT_TIMESTAMP默认值都可以。索引设计上我的原则是WHERE子句常用的字段建索引但不盲目加。用户表给username建唯一索引景点表给category和city建普通索引订单表给user_id建普通索引这样列表查询和按用户查订单都能走索引性能有保障。很多初学者会给每个字段都建索引结果写入变慢、索引文件膨胀完全没必要。2.3 初始化数据的准备技巧表结构设计完了接下来是准备测试数据。这个环节很多人不重视随便造几条数据就开写结果开发到列表分页时发现数据不够看效果又回头补数据很影响效率。我的做法是写一个init.sql脚本一次性把管理员账号admin/admin123密码是加密后的密文、10条景点数据、10条酒店数据、几条用户数据、几条示例评论都准备好。景点图片直接用网络图片URL占位比如随手找一些公网可访问的风景图链接开发阶段完全够用。等到部署上线前再替换成真实图片。另外MySQL的字符集和排序规则在建库时就要设好。强烈建议建库语句写成CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;采用utf8mb4_general_ci作为排序规则的原因是在查询中文和英文字符时比较速度更快。等用到ORDER BY中文排序或模糊搜索时你就能体会到字符集没设对的痛苦了。3. 后端核心实现与MyBatis实操要点3.1 SpringBoot项目搭建与分层架构后端部分我用Maven搭建的SpringBoot项目包结构按照Controller - Service - Mapper - Entity四层划分这已经是Java后端开发的行业标准照着写就行。controller接收HTTP请求参数校验调用Service层返回统一结果service业务逻辑层处理具体业务规则比如下单时校验库存、计算总金额mapper数据访问层定义接口方法配合XML文件写SQLentity数据库表对应的实体类字段和表字段一一对应这种分层的好处是职责单一出了问题能快速定位。比如订单金额算错了肯定先看Service层逻辑而不是在一坨代码里到处找某个SQL查询慢直接去Mapper XML里优化。项目还需要统一返回结果类和全局异常处理。返回结果类我定义为Result包含code状态码、message消息、data数据三个字段。正常请求返回code200业务错误返回code500或自定义错误码未登录返回code401。前端根据code统一判断并处理不需要每个接口单独处理异常。全局异常处理器用RestControllerAdvice注解实现拦截Service层抛出的业务异常和系统异常转换为统一的返回格式。这么做的好处是后端不会把异常堆栈直接暴露给前端既安全又规范。SpringBoot的配置文件application.yml里有几个配置必须注意server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 servlet: multipart: max-file-size: 50MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travel.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里解释几个关键配置的作用serverTimezoneAsia/ShanghaiMySQL 8.0以上版本连接数据库时如果不指定时区会报Server returns invalid timezone错误必须加上map-underscore-to-camel-case开启后数据库字段create_time能自动映射到实体类属性createTime不用每个字段都写TableField或别名mybatis log-impl控制台打印SQL语句开发阶段强烈建议开启能直观看到MyBatis执行了哪些SQL、传入了什么参数排查问题效率翻倍3.2 JWT登录认证与Token处理细节前后端分离项目没有Session概念因为前端和后端可能不在同一个域名下Session跨域非常麻烦。这套系统的登录认证用JWTJSON Web Token实现前后端通过Token传递登录状态。JWT的原理可以简化理解为用户登录成功后后端生成一个包含用户信息比如userId、username、过期时间的加密字符串返回给前端。前端把它存在localStorage里之后每次请求在请求头加上Authorization: Bearer 这个token。后端通过拦截器解密并校验token确认有效就放行无效就返回401。后端实现分三步第一步JWT工具类。我使用的是io.jsonwebtoken这个库工具类里包含生成token和解析token两个方法。生成时设置过期时间一般设定为7天这样用户登录后一周内不需要重复登录。第二步登录接口。用户提交用户名密码后先从数据库查出用户用BCrypt密码校验器比对密码注册时存入的是加密后的密文登录时用encoder.matches(rawPassword, encodedPassword)校验。校验通过后生成token返回同时返回用户基本信息不包含密码前端存下来用于展示用户名和头像。第三步拦截器校验Token。拦截器在SpringBoot里实现起来有两种方式实现HandlerInterceptor接口或在启动类实现WebMvcConfigurer注册拦截器。我在preHandle方法中从请求头取token解析失败则返回401解析成功则把userId存入request.setAttribute中方便Controller从request里获取当前登录用户。拦截器放行路径包括登录注册接口和景点列表查询等公开接口其他接口必须登录才能访问。前端配合做的事情也很重要axios请求拦截器在每个请求头上自动带上token响应拦截器检测到401时清除本地token并跳转登录页。这样整个登录认证闭环就通了用户不会看到后端返回的原始401报错而是被自动引导到登录页面。3.3 MyBatis使用中最容易踩的坑单个字符比较与批量插入MyBatis是个轻量的持久层框架但实际开发中有些细节特别容易踩坑我至少见过几十次有人在群里问相关问题。这里挑两个最常见的重点讲。单个数字字符比较问题这个问题的典型场景是前端传了一个字段status值是单个数字字符比如1后端用if teststatus ! null and status ! 做动态SQL判断时一旦写成if teststatus 1就报NumberFormatException。根本原因在于MyBatis的if标签使用OGNL表达式解析OGNL把1当成了char类型。在Java中char和String比较时OGNL会试图把char转成数字从而触发异常。解决方案有两种要么把判断条件写成if teststatus ! null and status 1注意外面单引号里面双引号让OGNL识别为字符串比较要么把1.toString()写上即if teststatus 1.toString()。最推荐的做法还是前者写法简洁直观。批量插入操作批量插入在旅游网站里的应用场景是后台管理系统需要一次性添加多个景点标签或批量导入数据。很多人会自己写循环单条插入结果性能堪忧每插入一条都要请求一次数据库。正确的姿势是使用MyBatis的foreach标签拼接SQLinsert idbatchInsert parameterTypelist INSERT INTO scenic (name, description, category, city, price) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.description}, #{item.category}, #{item.city}, #{item.price}) /foreach /insert这样最终执行的是一条INSERT INTO ... VALUES (...), (...), (...)语句一次网络请求就搞定全部插入效率能提升一个数量级。不过有个限制要注意MySQL默认max_allowed_packet参数限制了单条SQL的最大体积如果一次性插入的数据量特别大比如几万条需要分批处理每批500条左右比较安全。3.4 核心接口设计与实现示例接口设计这块我按业务模块来组织前后端按接口文档对齐字段名避免联调时来回扯皮。这里挑几个核心接口详细说下设计思路。景点列表分页查询接口请求方式GET /api/scenic/list?page1limit8category自然风光city北京这个接口需要返回分页数据和总数我封装了一个PageResult对象包含list和total两个属性。MyBatis分页我用的PageHelper插件用法很简单查询前调用PageHelper.startPage(pageNum, pageSize)查询后返回的PageInfo对象就自动包含了分页信息和数据列表。分页参数一定要做默认值处理前端不传page时默认为1不传limit时默认为8避免空指针异常。景点详情接口请求方式GET /api/scenic/detail/{id}返回景点所有字段信息、关联评论列表、该景点是否被当前登录用户收藏。评论列表和收藏状态不能写死需要根据当前用户ID查询。这个接口会从拦截器设置的request.getAttribute(userId)中取用户ID如果未登录则收藏状态返回false不需要强制登录。下单接口请求方式POST /api/order/create提交参数scenicId或hotelId、bookDate、quantity、contactName、contactPhone、type。Service层逻辑先根据ID查询景点或酒店是否存在且上架再计算总金额单价*数量生成订单号规则时间戳随机四位数字插入订单表返回订单ID。这里要注意事务控制用Transactional注解查询和插入任何一个环节失败都能回滚避免产生脏数据。景点搜索接口这是我个人认为最能体现MyBatis动态SQL优势的地方。前端传了关键词、城市、分类、价格区间等多个可选条件每个条件都可能为空。如果用JPA写这个查询条件组合会非常麻烦而MyBatis动态SQL用if标签就能优雅解决select idsearchScenic resultTypecom.travel.entity.Scenic SELECT * FROM scenic where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testcity ! null and city ! AND city #{city} /if if testcategory ! null and category ! AND category #{category} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY view_count DESC /select注意maxPrice的判断是! null而不是! null and ! 因为价格是数值类型传空字符串会导致类型转换错误。此外号在XML中需要转义为lt;这也是我刚开始写MyBatis时经常忘记的细节。4. 前端Vue实现与页面渲染关键点4.1 Vue环境搭建与项目初始化前端项目我用的Vue CLI脚手架搭建。这里先说一个新手特别容易卡住的点Node.js版本。Vue CLI 4.x对Node版本有要求Node版本太高或太低都可能安装依赖时报错建议使用Node 14.x或16.x的LTS版本。装完Node后用npm config set registry https://registry.npm.taobao.org切换镜像源不然安装依赖会慢到怀疑人生。项目初始化命令vue create travel-front交互式选项里选择Manually select features勾选Babel、Router、Vuex、Linter路由模式选择history这个选择后续部署时要注意配置Nginx后面我会讲到CSS预处理器选择Node Sass。项目创建完成后再安装ElementUI和axiosnpm install element-ui axios在main.js中全局注册ElementUI并引入样式文件。这里有一点经验ElementUI全量引入简单省事开发阶段完全够用不需要去做按需引入优化等真正遇到打包体积问题再处理不迟。4.2 axios统一封装与Token处理axios必须封装这是前后端分离项目的共识。不封装的后果就是每个组件里都重复写请求地址、错误处理逻辑代码冗余且难以维护。我的封装思路是创建一个request.js文件实现三件事。第一创建axios实例。设置基础URL为/api这样所有请求路径都简化为/scenic/list这种形式不用每次写全路径。超时时间设置为10秒。第二请求拦截器。从localStorage取出token如果有就加到请求头Authorization上。代码示意service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })第三响应拦截器。统一处理返回结果约定code为200时直接返回response.data.data剥离外层包装调用方拿到的就是业务数据code为401时清空localStorage并跳转登录页其他错误码通过Message.error弹出提示。这样业务代码里就不用写一大堆错误处理了只关注正常逻辑。4.3 路由配置与登录守卫机制Vue Router的路由配置分为两部分用户端页面和登录/注册页面。用户端页面包括首页、景点列表、景点详情、酒店列表、酒店详情、购物车/订单确认、个人中心等这些页面有些是公开的比如首页、景点列表有些需要登录才能访问比如个人中心、订单确认。路由守卫是处理登录状态的关键。我用beforeEach全局前置守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) const publicPages [/login, /register, /home, /scenic, /hotel] if (publicPages.includes(to.path)) { next() } else { if (token) { next() } else { next(/login) } } })需要登录才能访问的页面前端做了一次拦截后端接口还有一层鉴权双重保险。这里要说明的是前端守卫只是优化用户体验真正的安全校验必须在后端做因为别人完全可以直接用接口工具跳过前端调用后端接口。另一个容易忽略的路由细节是history模式。Vue Router默认是hash模式URL里会带#号不美观且对SEO不友好。改用history模式后URL变得干净清爽但部署到Nginx后必须配置try_files规则否则刷新或直接访问子路径时会报404。具体配置方式我在部署章节会给出完整示例。4.4 典型页面实现首页轮播、列表筛选、视频播放开发完通用配置后重点页面就是纯业务展示了。我挑三个有代表性的页面说下实现思路。首页轮播图。旅游网站的首页必须有视觉冲击力轮播图是最直接的方式。我用ElementUI的el-carousel组件实现数据来源是后端的轮播图接口。每个轮播图项包含图片地址和跳转链接点击后能跳转到对应景点详情页。轮播图的自动播放、切换动画参数根据视觉效果调整比如interval5000表示5秒切换一张。景点列表页。这是前后端交互最典型的场景。页面顶部是筛选条件栏分类、城市、价格区间、关键词搜索中间是景点卡片列表底部分页。筛选条件变化时重新发起请求并把参数传到后端。这里有一个交互细节搜索条件变了之后页码要重置回第一页否则停留在第5页筛选用户会看到空白列表我一开始就踩过这个坑。景点详情页的视频播放。旅游景点经常有宣传视频这个页面我使用的是vue-video-player组件。不过这里会涉及视频流格式问题如果视频文件是m3u8格式的HLS流vue-video-player需要配合videojs-contrib-hls插件才能播放。处理逻辑是这样的import video.js/dist/video-js.css import VideoPlayer from vue-video-player import videojs-contrib-hls Vue.use(VideoPlayer)播放器配置中播放m3u8流时type需要设为application/x-mpegURL否则视频加载不出来。这个点比较冷门但实际开发中视频宣传片用m3u8格式的情况很常见遇到了能省不少排查时间。5. 前后端联调、部署上线与高频问题排查5.1 跨域问题的本质与解决方案前后端分离项目必然遇到跨域问题这是浏览器的同源策略导致的。所谓同源是指协议、域名、端口三者完全一致。前端跑在http://localhost:8081Vue CLI默认端口后端跑在http://localhost:8080端口不同浏览器就会拦截跨域请求。解决方案有两种我开发环境用的代理转发生产环境用的Nginx反向代理。开发环境代理。在Vue项目根目录创建vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }原理是前端请求/api/scenic/list时Vue CLI的开发服务器会把请求转发到http://localhost:8080/api/scenic/list。因为服务端到服务端的请求不存在跨域限制所以问题迎刃而解。前端开发时请求的始终是同源地址代码里不需要任何跨域处理逻辑。后端CORS配置。除了前端代理后端也可以开启CORS跨域资源共享。SpringBoot中写一个配置类即可Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*); } }开发阶段直接把allowedOrigins设为*允许所有来源访问。生产环境建议改成实际域名提高安全性。5.2 生产环境部署完整流程部署我分为三块数据库、后端、前端。部署顺序也按这个来因为前后端都需要连数据库。数据库部署。生产服务器安装MySQL后执行初始化脚本创建数据库和数据表。注意调整MySQL的bind-address如果后端也部署在同一台服务器上保持默认的127.0.0.1即可不需要对外开放3306端口这样更安全。后端部署。执行mvn clean package打jar包上传到服务器然后用java -jar travel-server.jar --spring.profiles.activeprod启动。生产环境的数据库连接信息建议通过application-prod.yml单独配置不用改主配置文件。用nohup java -jar travel-server.jar log.txt 21 后台启动并写一个start.sh脚本管理启停。前端部署。执行npm run build构建会在dist目录生成纯静态文件。把dist目录上传到Nginx的html目录下Nginx配置如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; # 解决history路由刷新404 location / { 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-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;是history模式部署最关键的一行它把所有不存在的路径都重写到index.html由前端路由接管。/api路径反向代理到后端服务的8080端口同时解决了生产环境的跨域问题。把Nginx配好后访问服务器IP或域名就能看到网站首页了。整个部署链路我梳理成一张流程表环节操作关键命令/配置数据库导入init.sqlmysql -u root -p travel_db init.sql后端打jar包并启动mvn clean package -DskipTests后端进程管理后台启动并记录日志nohup java -jar xxx.jar log.txt 21 前端构建生成dist静态目录npm run buildNginx配置静态托管接口反向代理try_files / proxy_pass访问验证浏览器访问域名http://your-domain.com5.3 高频报错与排查技巧速查表我把这套系统开发过程中实际遇到的高频问题整理了一下每个都标注了解决方案你可以直接对照排查。报错信息可能原因解决方案Server returns invalid timezoneMySQL连接URL缺少时区配置URL加serverTimezoneAsia/ShanghaiAccess denied for user rootlocalhost数据库账号密码错误检查application.yml中的账号密码Whitelabel Error PageController路径写错或服务未启动检查控制台日志和RequestMapping路径Invalid bound statement (not found)Mapper接口和XML没有绑定检查XML文件路径是否在mapper-locations配置的目录下NumberFormatException: For input string: 1MyBatis单字符比较写法错误改用双引号包裹判断值Port 8080 was already in use端口被占用netstat -anoVue页面刷新404history模式和Nginx未配置try_files配置try_files $uri $uri/ /index.html;Failed to load resource: net::ERR_CONNECTION_REFUSED前端代理未配置或后端未启动检查vue.config.js代理和后端进程状态图片加载不出来图片路径是本地绝对路径或跨域使用相对路径或完整URL并配置静态资源映射Cannot read property xxx of null接口返回data为null页面未判空页面获取数据后先判断再渲染这里我还想分享一个排错顺序的经验遇到前端页面没数据或后端接口报错时先从网络请求下手打开浏览器F12的Network面板看请求是否发出、返回什么状态码、响应体是什么。绝大多数前后端联调问题在这一步就能定位到是前端没传参、后端报500、还是跨域拦截不需要着急去翻代码。如果有多个环节可能出错先用curl直接调后端接口确认后端正常后再排查前端的问题这样效率最高。5.4 项目后续扩展思路这套系统跑通之后并不是终点。从学习角度讲你可以在这个基础上扩展很多功能既能巩固技术又能增加简历或毕设的亮点。比较有价值的方向有三个。第一个是引入Redis做缓存把景点详情、热门推荐这些读多写少的数据缓存起来能显著提升响应速度。第二个是增加富文本编辑器让管理员在后台维护景区攻略、新闻资讯等图文内容扩展成内容管理模块。第三个是接入第三方登录微信扫码/支付宝登录或支付接口模拟支付即可让电商闭环更完整。技术层面还可以继续深挖。比如后端服务拆分成多个微服务模块加上Nacos注册中心和Gateway网关就变成了微服务架构项目前端可以引入TypeScript和Vite升级到Vue3体验新版生态。这些扩展既不会偏离旅游网站的业务场景又能逐步加深你对整个技术栈的理解。最后说点实在的我自己在部署这套系统时最大的体会是前后端分离项目的难点从来不是某个单独技术点而是整个链路的打通。从数据库设计时的字段类型到后端接口返回的JSON结构再到前端页面的数据渲染每一环都紧密相扣。遇到问题时画一条完整的数据流——浏览器请求从哪来、经过哪个代理、打到后端哪个接口、查了哪张表、返回什么结构、前端怎么解析——按图索骥绝大多数问题都能迎刃而解。这个排查思路本身比任何一个单独的技术点都值钱。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进