资讯动态

SpringBoot+Vue前后端分离景区导游平台实战:从建表到部署全解析

发布时间:2026/10/1 3:21:17 来源:尧图企业网站定制
这段时间我在整理本地项目备份翻到了今年年初做的一个景区导游平台就是给桂林这边几个景点做的游客服务系统。技术栈很常见SpringBoot Vue MyBatis MySQL整体是前后端分离架构。当时从数据库建模到前后端联调满打满算花了两周部署上线之后又修了几轮小问题项目才稳定下来。最近好多朋友在问这个项目的源码和部署方式我就把它整理成一篇完整的实操总结把建表思路、后端接口、前端路由代理、打包部署以及我踩过的坑一次性讲清楚。这篇内容适合两类人一类是刚学完 SpringBoot 和 Vue正缺一个完整项目练手的初学者另一类是在学校或者小公司需要快速交付前后端分离项目的开发者。对于前者我建议你从数据库设计开始顺着后端接口再到前端页面完整把项目跑起来对于后者你重点看部署章节和问题排查部分很多坑我已经提前帮你踩平了。1. 项目拆解桂林旅游景点导游平台到底在做什么1.1 项目定位与核心诉求游客、导游、管理员的三角业务我第一次接到这个需求时甲方说的是“搞一个桂林景点导游平台游客能看景点、约导游”听起来很简单。但真正梳理起来业务里其实站着三类人需求完全不一样。游客端要能浏览桂林景点比如漓江、象鼻山、芦笛岩看景点详情、开放时间、导游简介然后选择合适的导游下单预约。导游端要维护自己的服务日程查看游客下单记录确认或者拒绝订单。管理员端要维护景点数据、导游数据、线路信息还要能处理游客的反馈或者投诉。如果没有厘清这三个角色很容易把系统做成一个“带页面的增删改查工具”。我在实际建表之前先用一天时间把每个角色的核心诉求列成清单再对照清单去设计功能模块后面写代码基本没有返工。这个习惯我强烈建议你保留哪怕项目再小也值得先花半小时列出角色与诉求。系统最终需要完成的功能可以归纳成几个闭环游客注册登录、浏览景点列表与详情、按关键词搜索景点、查看导游排期、提交预约订单、导游确认订单、管理员维护景点和导游信息。这些功能全部落在一个典型的 B/S 架构里前端是单页应用后端提供 JSON 接口。1.2 核心业务流程从找景点到下单导游的完整闭环我用一条完整用户路径来帮你理解流程。游客进入平台首页展示桂林热门景点他点击其中一个景点进入详情页看到这个景点的介绍同时看到可预约的导游列表。选择一个导游后系统展示导游的可预约时段游客选好日期提交预约此时订单状态是“待确认”。导游登录自己的账号看到这笔订单点击确认后状态变为“已确认”游客在订单列表里就能看到自己的预约成功。这个闭环里有个容易被忽略的细节导游排期。如果只做订单表而不做排期表导游的时间冲突问题在后期会非常头疼。我做了一个guide_schedule表导游可以提前录入自己哪天可服务游客下单时只能选择可服务日期基本规避了超售和冲突。这个设计在演示项目里看起来“多了一张表”但放到真实运营场景中这是平台能不能用起来的关键。管理员在这条链路里做的事情就是提供基础数据。没有景点数据游客端就是空的没有导游数据预约功能就是摆设。所以管理后台的功能优先级是最高的我建议在开发排期上把管理端放在游客端之前。2. 技术选型心得为什么是 SpringBootVueMyBatisMySQL2.1 前后端分离不是炫技这个项目为什么必须拆开做最近很多人提“前后端分离项目实战”但你要明白拆开不是目的解决问题的才是。这个导游平台选择前后端分离我当时的判断有三个。第一游客端将来大概率要出小程序或者移动端。如果后端接口和数据模型直接跟前端页面绑定将来做小程序就得把接口重新拆一遍等于挖坑填坑。前后端分离之后后端只要提供稳定 JSON 接口小程序、App、PC 端都能直接复用。第二前端页面里有大量列表、筛选、交互动效和后端 Java 代码放在一个工程里会让部署和编译变得很痛苦。前端构建产物是静态资源后端是Java进程两者独立部署、独立扩容线上出问题也好定位。第三开发协作更高效。我自己一个人做项目时体会不明显但如果是小组一起开发前端和后端各管一个目录互不打扰联调阶段只需要对接口文档。这种模式更贴近现在真实企业里的开发流程。当然前后端分离也有代价最典型的是跨域问题。开发环境下前端跑在 8080 端口后端跑在 8081 端口浏览器会拦截跨源请求。我们的做法是后端配置全局跨域前端通过代理解决。这个坑我后面会专门说。2.2 技术栈逐个点评四个框架组合的真实理由SpringBoot 在这个项目里的角色是“后端底座”。我用它不是因为新而是因为它内置了 Tomcat通过 starter 机制把配置简化到极致。一个spring-boot-starter-web就把 Web 环境、JSON 序列化、嵌入式服务器全带齐了省去搭环境的功夫。Vue 负责前端页面。我选 Vue 的核心原因是它上手曲线平滑组件化和响应式数据模型正好适合做这种典型的管理型加展示型混合项目。项目里有大量表单、列表、状态切换Vue 的双向绑定让代码量减少很多同时它也方便做移动端适配。MyBatis 在这个项目里是最有争议的一项。很多新手会问都用 SpringBoot 了为什么不用 JPA 或者 MyBatis-Plus我的答案是这个项目的查询复杂度用 MyBatis 的 SQL 可控性是更好的。景点列表要做多条件筛选、导游订单要做多表联查MyBatis 允许我手写 SQL一眼就能看到实际执行内容排查问题很快。MyBatis 缓存机制也做得不错一级缓存是 SqlSession 级别的二级缓存是 namespace 级别的对于这种读多写少的业务场景非常合适不过缓存粒度要控制好后面我会讲怎么踩坑。MySQL 就更不用多说了轻量、稳定、运维成本低。这个项目的数据量也就是几十万条以内单库单表完全够用。不要一上来就考虑什么分库分表那是过度设计。至于 SpringBoot 的版本我吃过亏。一开始我用了当时最新的 3.x结果发现部分第三方依赖还没适配比如某些数据库驱动版本不兼容师妹同学用 JDK 8 根本编译不过。这个项目我最后用的是 Spring Boot 2.7.x配合 JDK 8兼容性和稳定性都很好。如果你也打算把这个项目跑起来先把版本对齐再说后面的话。3. 数据库设计一张表一张表把景点业务钉死3.1 用户与角色模型不用 Spring Security 也能做好权限很多新手做权限控制第一反应是引入 Spring Security JWT然后被配置搞到崩溃。实际上对于这种体量的业务系统用户表里直接放一个role字段再配合后端拦截器做接口权限校验完全够用。我的sys_user表设计如下CREATE TABLE sys_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt密文, nickname varchar(50) DEFAULT COMMENT 昵称, phone varchar(20) DEFAULT COMMENT 手机号, role tinyint NOT NULL DEFAULT 2 COMMENT 角色: 1管理员 2游客 3导游, avatar varchar(255) DEFAULT COMMENT 头像地址, status tinyint NOT NULL DEFAULT 1 COMMENT 状态: 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段我强烈建议用 BCrypt 加密不要用 MD5。MD5 可以被彩虹表直接查询恢复安全性几乎没有。Spring Security 的BCryptPasswordEncoder可以单独提取出来用不用集成整套安全框架。role字段虽然简单但结合 JTW 里的角色信息完全可以在后端做拦截导游接口校验角色为 3管理员接口校验角色为 1。这种模式在教学项目和小型项目中都很常见等真的发展出复杂的权限层级比如多角色、菜单级别权限再引入 Spring Security 也不迟。3.2 景点、线路、订单、评论表设计字段背后的取舍景点表是这个平台的“地基”我设计字段时重点考虑了两个维度一是展示性二是检索性。CREATE TABLE scenic_spot ( id int NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 景点名称, image varchar(255) DEFAULT COMMENT 封面图, description text COMMENT 景点介绍, region varchar(100) DEFAULT COMMENT 所在区域如桂林市/阳朔, ticket_price decimal(10,2) DEFAULT 0.00 COMMENT 门票价格, open_time varchar(50) DEFAULT COMMENT 开放时间, rating decimal(3,1) DEFAULT 5.0 COMMENT 评分保留一位小数, status tinyint DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;rating字段我选择冗余存储在景点表里而不是每次通过评论表聚合计算。原因很简单列表页展示所有景点时如果每个景点都去做一次AVG(rating)的子查询数据库压力会很大尤其在数据量上来之后。我的做法是用户提交评论时同时更新景点表的rating字段用过度简单化的方式换来查询速度。这种“冗余字段”设计思路在真实项目中非常常用。订单表是整个系统里最关键的表因为它涉及状态流转。CREATE TABLE tour_order ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id int NOT NULL COMMENT 游客用户ID, guide_id int NOT NULL COMMENT 导游ID, spot_id int NOT NULL COMMENT 景点ID, schedule_id int DEFAULT NULL COMMENT 排期ID, visit_date date DEFAULT NULL COMMENT 游玩日期, price decimal(10,2) DEFAULT 0.00 COMMENT 订单金额, status tinyint DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, remark varchar(255) DEFAULT COMMENT 游客备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_no我使用时间戳加随机数生成避免自增ID直接暴露订单数量。status是一个 tinyint而不是字符串这有利于索引和比较。很多同学喜欢存“待确认”这种中文文本那样做不但浪费空间而且后续改状态逻辑特别麻烦。业务代码里用枚举或者常量类去映射这个数字含义就好。还有一个表容易被忽视guide_schedule导游排期表。我设计它的初衷是让订单有据可依。CREATE TABLE guide_schedule ( id int NOT NULL AUTO_INCREMENT, guide_id int NOT NULL, spot_id int DEFAULT NULL, service_date date NOT NULL COMMENT 可服务日期, status tinyint DEFAULT 0 COMMENT 0空闲 1已占用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次游客下单时后端会先查询对应guide_id和service_date的排期状态如果已经是 1就直接拒绝下单并提示“该导游此日已被预约”。这个逻辑写起来很简单但能避免很大一部分业务纠纷也是面试官最喜欢问的点。4. 后端实现从登录鉴权、景点查询到导游预订的完整链路4.1 工程结构与请求链路Controller-Service-Mapper怎么分层我建 SpringBoot 工程时包结构固定是以下这种风格com.guilin.tour ├── controller // 接收请求、参数校验、返回结果 ├── service // 业务逻辑 ├── mapper // MyBatis数据访问接口 ├── entity // 数据库实体 ├── dto // 前端交互对象 ├── common // 通用类结果返回、常量、异常 └── config // 配置类跨域、JWT拦截器请求链路就是标准的 Controller - Service - Mapper - MySQL。很多同学纠结“为什么还要写 Service 层Controller 直接调 Mapper 不行吗”。我的理解是Service 层是放置“业务判断”的地方。比如下单时查排期状态、更新排期状态、生成订单号这三个动作必须在一个事务里完成。如果将这段逻辑散落到 Controller 或者 Mapper 层后续维护就是噩梦。事务控制我直接用Transactional默认遇到运行时异常回滚。这里要注意Transactional默认只对RuntimeException回滚如果你方法里直接throw new Exception它是不会回滚的。我第一版就踩过这个坑游客下单后排期占用成功了但订单创建失败导致排期被锁死。4.2 登录与JWT鉴权、参数校验、异常处理核心代码登录的流程其实很经典前端把用户名密码发给后端后端校验通过后生成 JWT返回给前端前端后续请求在 Header 里带上token后端拦截器解析 token 取出用户信息。JWT 工具的代码很固定核心是生成和解析。// 生成 JWT public String createToken(Integer userId, String username, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }注意这里我设置了 24 小时过期时间。实际项目可以按需调整比如管理端最好 2 小时过期游客端可以 7 天免登录。过期时间设置的逻辑要对应前端处理前端请求时如果后端返回 401就自动跳转登录页并清掉本地 token。我选择自己写 JWT 拦截器而不是引入 Spring Security 的原因主要是减少学习成本和配置复杂度。当然这建立在系统本身角色简单的假设上你不能拿它去套一个几十个权限点的复杂系统。我用一个HandlerInterceptor拦截/api/**路径在preHandle里解析 token然后放入ThreadLocal或者直接放请求属性里供后续代码获取当前用户。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, Integer.parseInt(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; }这个写法是展示核心代码如果你要直接复制到项目里建议把 token 的存储方式和过期策略在application.yml里做成可配置项。参数校验我用的Validated加上 DTO 里的注解比如NotBlank、NotNull。返回统一使用一个ResultT对象格式是{ code, message, data }。这样前端 Axios 拦截器解析响应时非常舒服不用每个接口单独处理状态。4.3 MyBatis动态SQL与多表联查搜索、排序、分页就这么完成景点列表是个典型的多条件查询场景可能有名称模糊搜索、区域筛选、价格区间、评分排序。这种情况下 MyBatis 动态 SQL 是最好用的。select idsearchSpots resultTypecom.guilin.tour.entity.ScenicSpot SELECT * FROM scenic_spot where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testregion ! null and region ! AND region #{region} /if if testminPrice ! null AND ticket_price gt; #{minPrice} /if if testmaxPrice ! null AND ticket_price lt; #{maxPrice} /if AND status 1 /where ORDER BY choose when testsort ratingrating DESC/when otherwisecreate_time DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /selectwhere标签会自动处理 SQL 里多余的和 AND避免出现WHERE AND name LIKE这类语法错误。choose标签则用于实现排序字段的白名单映射。我见过直接把排序字段拼进 SQL 的写法和 JPA 的一些场景建议一律不要使用因为ORDER BY这里是不能使用预编译占位符的必须自己做好映射白名单。分页我这里手动计算了offset。也许有人会推荐 PageHelper它很好用但在这种多表联查场景下要格外小心它发的 count 查询。我这个项目数据量不大手动LIMIT更简单可控。订单模块的多表联查natural是理解 MyBatis 的关键。导游在后台看到自己的订单列表往往需要同时显示游客昵称和景点名称这就需要把tour_order、sys_user、scenic_spot三张表 join 起来。select idselectGuideOrders resultTypecom.guilin.tour.dto.OrderDetailDTO SELECT o.id, o.order_no, o.visit_date, o.status, o.price, u.nickname AS userNickname, s.name AS spotName FROM tour_order o LEFT JOIN sys_user u ON o.user_id u.id LEFT JOIN scenic_spot s ON o.spot_id s.id WHERE o.guide_id #{guideId} ORDER BY o.create_time DESC /select写 JOIN 时的原则是“明确主表”。这里主表是订单表其他表都是补充信息所以用 LEFT JOIN 包住了右侧可有可无的信息用户理论上一定有可以换 INNER JOIN但 LEFT JOIN 更保险景点被删了也能查出订单记录。MyBatis 的缓存之前提过二级缓存需要手动开启。我在这个项目关闭了二级缓存因为景点数据修改频率不高但缓存后导游修改排期和订单状态时可能会查到脏数据由于是多表联查二级缓存极易出现数据不一致。多表 JOIN 查询的 SQL 尽量不要开启二级缓存这是一个很重要的一句话总结。5. 前端实现Vue生态下把页面和数据拉通5.1 环境准备Node、Vue CLI/Vite、依赖安装避坑前端你首先要确认 Node 环境。我个人经验是 Node 版本别追新尤其是用 Vue CLI 创建的项目Node 版本太高反而会导致 node-sass 编译失败。建议直接用 npm 安装 sass 时选择sass或者dart-sass避开 node-sass 这个坑。我从那批网络热词里看到不少人问“vue安装及环境配置”“vscode vue 怎么制作手机软件”这里统一回答一下。如果你是用vue/cli来建项目执行下面命令npm install -g vue/cli vue create tour-front创建项目时选择 Vue 2 还是 Vue 3我的建议是如果这个源码项目是基于 Vue 2 Element UI 的你就用 Vue 2如果是从零开始直接用 Vue 3 Vite Element Plus。但要注意Vue 2 升级到 Vue 3 改动很大API 风格都变了不要指望直接替换包版本就能跑起来。依赖安装最怕装到一半报错。一个保险做法是使用npm config set registry https://registry.npmmirror.com切到国内镜像源可以极大减少超时问题。如果你拿到的项目出现ERESOLVE错误试试先npm install --legacy-peer-deps多半能过关。5.2 路由与动态权限、Axios统一封装、页面组件拆分前端路由我用的是 Vue Router。经典的访问地址包括/home、/spot/:id、/login、/admin。游客端和管理端的权限区分我通过路由守卫做校验。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.role token) { const user JSON.parse(localStorage.getItem(user) || {}) if (user.role ! to.meta.role) { next(/403) } else { next() } } else { next() } })不过是简单的角色控制真的没必要用动态路由写好路由元信息就够了。如果你从“vue动态路由”的搜索词里来了要知道动态路由主要解决的是后端菜单权限变化场景当前项目规模不需要把它用上。真要实现也是后端返回菜单列表前端addRoute动态注册同时要处理刷新后路由丢失问题至少要把路由表存在 Pinia 或 localStorage 里。Axios 封装是前端的“基础设施”。我习惯把 baseURL、token 注入、响应拦截全部集中在一个request.js文件。import axios from axios const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, 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( res { // 后端统一Result结构 if (res.data.code 200) { return res.data.data } return Promise.reject(new Error(res.data.message)) }, err { if (err.response err.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(err) } )注意这里baseURL我留了环境变量。开发环境配成http://localhost:8081/api生产环境通常走 Nginx 反向代理可以直接写成/api让 Nginx 把请求转发给后端服务这样就没有跨域问题了。页面组件拆分方面我建议把公共部分抽出来景点卡片、订单状态标签、评论列表每个抽成一个单独文件。这样做不只是为了复用而是为了后续维护方便。比如你想给景点卡片加个收藏按钮只改一个组件所有用到它的页面都会更新。6. 源码部署从本机启动到服务器上线全流程实录6.1 后端启动的3种方式IDE、jar包、外部Tomcat的war包先说本地启动。在 IDEA 里打开后端项目等 Maven 把依赖全下载完配置好application.yml里的数据库连接直接运行启动类即可。要注意 Spring Boot 版本和 JDK 版本要匹配Java 8 对应 Spring Boot 2.xJava 17 对应 Spring Boot 3.x。然后是 jar 包部署这是最快的方式也是我最推荐的方式。mvn clean package -DskipTests java -jar target/tour-server-1.0.0.jar如果你服务器有公网 IP 和域名将 jar 放上去执行nohup java -jar即可。记得在application.yml里把端口改成8080或者别的不会被防火墙拦截的端口。如果你想部署到外置 Tomcat比如服务器已经跑了一个 Tomcat 且不想再开 Java 进程那么有两种写法。首先在启动类上继承SpringBootServletInitializer并重写configure方法SpringBootApplication public class TourApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(TourApplication.class); } }然后在pom.xml里把打包方式改为 war并将spring-boot-starter-tomcat的依赖设置成scopeprovided/scope。这样打出来的 war 包丢到 Tomcat 的webapps目录启动 Tomcat 会自动解压部署。注意war 包方式和 jar 包方式不要混用。如果你已经用 jar 启动了系统又去把一个相同的应用部署到同一台机器的 Tomcat端口冲突会让你怀疑人生。6.2 前端构建、Nginx代理与静态资源部署前端开发跑起来是npm run serve或者npm run dev但生产环境一定是要构建成静态文件后交给 Nginx 托管。执行npm run build构建完成后项目根目录会生成dist文件夹里面是静态资源。我把dist里的文件传到了服务器/var/www/tour目录然后配置 Nginx 的站点。实际上“tomcat部署前后端分离项目”的搜索需求在 SpringBoot 内置 Tomcat 的时代含义已经变了。后端是一个 SpringBoot 进程前端是静态资源不需要把前端文件塞进 Tomcat。下面是一个可用的 Nginx 配置模板server { listen 80; server_name tour.example.com; root /var/www/tour; index index.html; # 前端路由 history模式刷新时找不到404的问题 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这段配置第一处核心是try_files $uri $uri/ /index.html解决 Vue Router history 模式刷新 404 的问题。第二处是/api/代理到后端服务。前端请求/api/scenic/list经过 Nginx 会转发到http://127.0.0.1:8081/api/scenic/list后端接口别忘记在 Controller 上加上统一的/api前缀或者配置context-path。如果你不想让后端地址暴露给前端这种代理方式是标准做法也是很多人叫的“反向代理”。好处是浏览器只跟 Nginx 通信后端端口不用对外开放安全性和灵活性都有提升。6.3 数据库初始化MySQL安装、Navicat导入和账号配置部署里最容易翻车的部分是数据库。如果你是新装 MySQL无论 CentOS 还是 Ubuntu安装后第一件事就是确认 root 密码能正常登录。我用 Navicat 连接 MySQL 时曾碰上一个经典问题MySQL 8 默认的加密规则是caching_sha2_password老版本 Navicat 不认报错信息类似“Authentication plugin caching_sha2_password cannot be loaded”。解决方案有两条换新版本 Navicat或者修改用户加密规则。mysql 里执行ALTER USER root% IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;数据库初始化可以不用 Navicat直接用命令行导入也行mysql -uroot -p create database tour_db default character set utf8mb4; use tour_db; source /data/sql/tour.sql;导入之后一定要检查表结构是否完整特别是看看有没有把外键关联、索引建上。我见过有人把 SQL 文件直接拖到 Navicat 里双击执行结果部分语句失败没有及时发现导致后期启动项目时各种“Unknown column”。导入成功后建议顺手执行一条show tables;。后端连接 MySQL 时application.yml中有三个关键配置容易出错spring: datasource: url: jdbc:mysql://localhost:3306/tour_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456serverTimezoneAsia/Shanghai解决时区报错useSSLfalse规避 SSL 加密连接问题allowPublicKeyRetrievaltrue是 MySQL 8 在连接时可能需要的。网络热词里提到“mysql ssl连接错误”大概率就是你没加useSSLfalse。7. 常见问题排查12个真实坑与对应解法我把自己开发这个项目和帮朋友跑源码时遇到的高频问题汇总了一下这些问题如果你提前知道能节省至少两天排错时间。7.1 数据库连接类报错时区、SSL、密码、驱动版本第一类问题集中在数据库连接。比如启动时直接抛The server time zone value йʱ is unrecognized这通常是没配serverTimezone。直接改成serverTimezoneAsia/Shanghai即可。如果你用的是 UTC存进去的时间可能会偏移 8 小时所以我建议直接写Asia/Shanghai。再比如提示Public Key Retrieval is not allowed需要在 URL 上加上allowPublicKeyRetrievaltrue。这是 MySQL 8 驱动条款导致的加了之后基本不会再报。如果你看到Access denied for user rootlocalhost基本都是账号密码错误。这种错误最容易发生在你用 Docker 启动 MySQL 时容器内配置的密码和你在本地 Navicat 输入的不一致。我那个项目换成 Docker MySQL 后就在这个错误上卡了一个小时。7.2 跨域、路由刷新404、端口占用与部署类问题跨域报错几乎每个前后端分离项目都会遇到。开发环境下最简单的后端解决方式是配置一个全局 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }但要注意allowCredentials(true)时allowedOriginPatterns不能写*某些浏览器会拒绝必须用allowedOriginPatterns(*)。另外如果你自己写了多个过滤器处理跨域可能导致OPTIONS请求被拦截返回 401前端一直显示跨域。后端如果同时存在跨域配置和 JWT 拦截器拦截器需要放行OPTIONS请求。前端 Vue Router history 刷新 404几乎是部署阶段新手必踩。本质是浏览器直接访问/home路径Nginx 找不到对应文件把请求交给了 404。解决方案就是前面写的try_files $uri $uri/ /index.html。端口占用问题也很常见。后端启动时Port 8080 was already in use可以用命令查一下被哪个进程占用netstat -tunlp | grep 8080 kill -9 进程号如果你同时启动了前后端开发服务器前端默认 8080后端默认 8081就不会打架。但如果后端也用 8080就会产生冲突。排错时先看端口再查日志效率最高。7.3 MyBatis相关的坑SQL日志、反编译修改、缓存优先级MyBatis 查不到数据时第一件事不是改 SQL而是开 SQL 日志。在application.yml里加logging: level: com.guilin.tour.mapper: debug这样控制台会直接打印 MyBatis 生成的 SQL 语句和参数你能一眼看到 SQL 执行的实际情况。排查慢查询、数据不一致时这个配置是救命稻草。关于热搜词里“怎么将springboot jar反编译成项目”我说点实际经历。拿到一个没有源码的 jar 包可以用 JD-GUI 直接反编译 class 文件也可以用 Maven 反编译插件。但反编译只能还原代码逻辑注释、配置文件里的临时修改并不完整。如果你接手的是别人打好的 jar我更建议直接在原工程里改配置重新打包而不是去反编译。反编译的代码只能应急看看想要长期维护还是要拿到正确的源码工程。我第一次折腾反编译就发现Lombok 生成的 getter/setter 反编译后一堆冗余代码可读性很差。最后强调一个 MyBatis 缓存的心得MyBatis 一级缓存默认开启同一个 SqlSession 内重复查询走缓存这在日常接口里问题不大二级缓存默认关闭我有一次手贱开启后订单状态更新不及时因为缓存的 key 是 namespace 级别的多表 JOIN 查询会把旧数据也缓存下来。多表 JOIN 查询的 SQL 建议关闭二级缓存结果集中的数据如果同时被多个表影响缓存就不是加速而是毒药了。7.4 部署与前后端版本的一体化排查清单我把疑难问题做成一个速查表方便你对照处理。现象可能原因解决方案后端启动后接口 404没加/api前缀或 controller 扫描路径不对检查启动类SpringBootApplication所在包是否覆盖 controller前端页面白屏路由模式与 Nginx 不匹配 / 静态资源路径错误检查history模式 try_files检查publicPath前端请求接口 401token 失效或未携带刷新 token检查 Axios 拦截器登录后页面刷新就退出用户信息只存在内存没持久化登录成功后把用户信息存 localStorageNavicat 连接 2059 错误MySQL 8 认证插件兼容问题修改用户为mysql_native_password上传图片无法显示静态资源映射没配置后端配置/upload/**映射到本地目录修改代码不生效没重新构建 jar 或前端没重新 build后端正确定版本号重启前端重新 build以上这些坑很多不是写代码时出现的而是环境差异造成的。同一份源码在不同 JDK、Node 版本、MySQL 版本下表现可能完全不同。所以我文章开头就强调先把版本对齐这是减少问题的最有效手段。最后说点主观感受。这个项目从数据结构到前后端实现难度其实不高但真正把它部署上线、让游客和导游用起来需要处理大量细节。我个人最大体会是技术栈选主流但够用比选热门但复杂更重要。我没有引入 Spring Security、Redis、RabbitMQ 这些外围组件不是它们不好而是当前项目体量容不下这些复杂性。作为开发者也一样不要为了“技术先进”去无脑堆积框架先把业务跑通等项目真的到了需要高性能和复杂权限的阶段再演进架构也不迟。如果你准备照着这份文档把项目搭起来我的建议是先不要追求跑通全部功能而是把数据库导入、后端启动、前端启动这三步完成看到前后端连通的数据了再逐步对照章节理解每一块代码。这样节奏感会好很多排查问题也知道该往哪个方向找。

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

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

免费获取报价 →
↑