先说个背景。去年我帮桂林本地一家做地接业务的旅游公司搭了套“旅游景点导游平台”,技术栈选了 SpringBoot + Vue + MyBatis + MySQL,前后端完全分离,从数据库设计、接口联调到最终部署上线,整套流程走了一遍。这个系统功能上没有特别炫酷的东西,核心就是景点信息展示、导游服务对接、线路规划、订单流转这些常规业务,但真把它从零搭起来并稳定运行在生产环境,需要处理的细节比想象中多。
这篇文章不打算写成教科书,而是按项目实操的顺序来讲:架构怎么拆、表怎么建、SpringBoot 和 MyBatis 哪些配置必须认真对待、Vue 这边有哪些坑、部署阶段用什么方案最省心,最后附上调试阶段最常见的问题。你手上如果有类似“××旅游平台”的项目要做,或者正准备在企业里做前后端分离项目的地部署上线,这篇文章可以直接拿来当操作手册参考。
1. 项目整体设计与架构分析
1.1 为什么是前后端分离,而不是传统 JSP 模板
做导游平台的时候,同行群里有人问:你们这系统又不复杂,用 SpringBoot 套个 Thymeleaf 或 JSP 模板,一个人全干完不就行了?非得分前后端,图什么?
这个问题的答案,做过实际项目的人都懂:开发分工、部署方式、后续扩展这三件事直接决定了选型。
前后端分离最直观的好处是开发不用互相等。前端同事拿到接口文档后可以自己用 Mock 数据先把页面做出来,后端专心设计表结构和业务逻辑,两边并行推进,联调阶段再合体。尤其是导游平台这种业务,旧系统在演示的时候被客户发现“景区天气模块想加在首页”,如果前端页面和后端渲染逻辑耦合在一起,这种需求改动会牵扯到控制器、模板、静态资源多层修改,分离之后前端只改一个组件,后端只管提供天气数据接口,改动范围被严格限制住了。
还有一个务实的理由:项目要交付源码,未来可能接微信小程序、App 甚至第三方分销系统。前后端分离后,后端接口天然就是一套 HTTP API,小程序直接复用,App 也直接复用,不需要再为了新的客户端单独写一套模板渲染逻辑。我在系统里把所有的 API 都设计成 JSON 格式,后面加了一个基于 uni-app 的小程序管理端,开发成本几乎只集中在前端。
从部署角度看,分离不等于复杂。很多人误以为前后端分离必须要上 Nginx、Docker 之类的东西,其实这是认知误区。小型项目完全可以前端打包成一个静态目录,后端还是 Java 进程,两者在物理上仍然可以分开部署,也可以合在一起跑,部署弹性比传统模板方案大得多。这篇文章后面会详细讲两种部署方式的取舍。
1.2 技术栈选型与项目目录结构
本项目的技术组合如下:
- 后端:SpringBoot 2.7.x + MyBatis 3.5.x + MySQL 8.0
- 前端:Vue 2.7(Composition API 也可以用,但这里用了 Vue 2 生态更稳)+ Vue Router + Axios + Element UI
- 构建:Maven 3.6+ 、npm 8+
- 中间件(可选):Redis 做会话共享,不强制,单机部署用默认 Session 也能跑
选 SpringBoot 2.7 而不是 3.x,主要是考虑到 MyBatis 相关 starter 的兼容性。SpringBoot 3 基于 Jakarta EE,很多老版本 MyBatis 依赖在替换 javax 到 jakarta 上容易踩坑。如果项目是团队内自用,用更稳定的 2.7 明显更省心。同理,MySQL 使用 8.0 是因为 5.7 社区版已停止更新,新项目没必要再抱着老版本。
Maven 项目结构是典型的分层结构:
gui-server/ ├── src/main/java/com/gltour/ │ ├── controller/ # 控制器层,只做参数接收和返回 │ ├── service/ # 业务逻辑层,事务控制 │ ├── mapper/ # MyBatis 接口层 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 前后端交互对象 │ └── config/ # 跨域、事务、静态资源配置 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 文件 │ ├── application.yml │ └── sql/init.sql # 建库建表脚本 └── pom.xml前端项目是脚手架生成的标准结构,重点目录如下:
gui-web/ ├── src/ │ ├── api/ # 统一封装的请求模块 │ ├── router/ # 路由配置文件 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ ├── store/ # Vuex 状态管理 │ ├── utils/request.js # Axios 实例配置 │ └── main.js ├── vue.config.js # 开发代理配置、打包路径配置 └── package.json这个结构的核心原则是“按业务边界切模块,而不是按技术类型堆文件”。前期已经吃过亏,一开始把所有 Controller 都放在一个包里,项目经理说“导游和景点管理的权限最好分开”,结果拆包时翻了几十个文件。这次一上来就按 module 拆好,后面维护舒服得多。
2. 数据库建模与核心表设计
2.1 桂林景点数据模型,这样设计才够用
导游平台的第一步,也是最容易被轻视的一步,是数据库建模。很多初学者上来直接写字段,不考虑业务场景,等做到“路线推荐”功能时就发现景点表缺地址、缺经纬度、缺开放时间,返工成本非常高。
桂林的景点业务有一个典型特点:景区之间存在“套票”和“联游”关系,比如漓江和遇龙河经常被安排在同一天,还有不同季节景点开放时间完全不同(夏季和冬季差半小时)。所以景点表除了基础字段外,我单独设计了两个支撑字段:
open_time和close_time:用VARCHAR(10)存“08:00”这类字符串,方便显示和比较,不要单纯建两个DATETIME字段。geolocation:存经纬度字符串,前端地图展示时直接解析。
核心表结构如下(精简版):
CREATE TABLE `scenic_spot` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '景点ID', `name` VARCHAR(64) NOT NULL COMMENT '景点名称', `cover_url` VARCHAR(255) DEFAULT NULL COMMENT '封面图地址', `description` TEXT COMMENT '景点介绍', `open_time` VARCHAR(10) DEFAULT '08:00' COMMENT '开放时间', `close_time` VARCHAR(10) DEFAULT '18:00' COMMENT '闭园时间', `ticket_price` DECIMAL(10,2) DEFAULT 0.00 COMMENT '参考票价', `category` TINYINT DEFAULT 0 COMMENT '类型:1-自然风光 2-人文古迹 3-主题乐园', `status` TINYINT DEFAULT 1 COMMENT '上下架状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;用TEXT类型存景点介绍,是考虑到某些热门景区的介绍文字录入量很大,并且会迭代更新。索引方面,name字段不要加全文索引,规模不大时LIKE '%关键词%'足够用,等数据量真到了几十万再考虑全文检索,现阶段不要过度设计。
2.2 导游、订单、线路关联表怎么建才不绕
导游、订单、线路这三块是业务核心,关系不算复杂,但容易出逻辑错误。我最终的表设计如下:
guide:导游信息表,字段包括name、phone、id_card_no(身份证号,实际项目里不要直接明文展示)、introduce(简介)、status(空闲/带团)等。travel_line:线路表,一条线路包含多个景点,所以有中间表line_scenic_ref关联line_id和scenic_id,再加一个sort_order字段表示游览顺序——很多新手会漏掉这个顺序字段,等到了前端要展示“第1站、第2站”时就傻眼了。combination_order:订单表,关联游客(customer_id)、导游(guide_id)、线路(line_id)、出行日期、状态。
一个关键设计是,订单状态不要在代码里硬编码数字魔法值。我的做法是在entity层定义一个状态枚举常量类,例如:
public class OrderStatusConst { public static final int PENDING_PAYMENT = 10; public static final int PAID = 20; public static final int IN_SERVICE = 30; public static final int FINISHED = 40; public static final int CANCELLED = 50; }之所以用 10/20/30 这种间隔数值,而不是 1/2/3,是为了后续扩展状态时不用改数据库字段和既有历史数据。比如说中途希望加一个“退款中”的状态,就插到 25,不需要调整别的状态码,这也是在实际项目中总结出来的经验。
2.3 建表后必须做的初始化动作
工程项目里,数据库脚本不要只靠 Navicat 点鼠标导入,一定要维护成 SQL 脚本文件放入项目,团队协作才能保持一致。我建议脚本里包含两类内容:建库建表 + 基础数据。
基础数据尤其重要,否则前端联调没数据可用。导游平台的初始数据包括:默认的 3 个测试账号(管理员、导游、普通游客)、10 条左右桂林景点数据、2~3 条经典线路数据。方便的话在data.sql里写清楚,前端一启动就能看到界面有内容,不至于白屏让你以为接口写错了。
这个初始化脚本还有第二个作用:用于讲部署的时候,可以快速演示整套流程,后面第 5 节部署环节会再次提到它。
3. 后端 SpringBoot 实现细节
3.1 基础配置别被版本坑——application.yml 关键项
SpringBoot 的配置看似简单,但坑往往藏在细节里。以我这个项目为例,application.yml中有几个地方值得展开说:
server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/gui_travel?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.gltour.entity configuration: map-underscore-to-camel-case: true logging: level: com.gltour.mapper: debug先说driver-class-name。如果你用 MySQL 8.0 的驱动,必须是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver。这一点在引依赖时就要确认,版本不对直接抛启动异常。
useSSL=false,这个参数网络上一堆人问。本地开发环境没有配置 SSL 证书,MySQL 8 默认会尝试走 SSL 连接,所以必须显式关闭,否则报SSL connection error。生产环境如果数据库没有开启 SSL,也建议显式关闭,避免每次连接都多一次握手耗时。
serverTimezone=Asia/Shanghai,不设这个会有时区警告,并且插入数据库的时间会偏移 8 小时。Java 的LocalDateTime与数据库DATETIME的匹配在 MySQL 8 上需要注意时区一致性,时区不对最明显的表现是“订单创建时间变成前一天”。
mybatis.map-underscore-to-camel-case这一项我强烈建议开启。默认情况下,数据库字段ticket_price映射到 Java 属性时如果没开启驼峰转换,查出来是null。开启之后 MyBatis 自动完成ticket_price到ticketPrice的映射,省去写大量resultMap的工夫。但这个配置只对查询结果的自动映射有效,resultMap里显式写好的映射不受影响。
3.2 MyBatis 配置的必配项和缓存问题
MyBatis 在课堂作业里通常只需要会写 Mapper 接口和 XML,但真实项目里,配置层面的东西要复杂一些。总结一下我认为必须处理的三件事。
第一件,Mapper 接口与 XML 路径的匹配规则。确保 Mapper XML 的namespace与接口全限定名一致,否则 Spring 注入时会报Invalid bound statement (not found)。这是一类特别常见的问题,排查方式在调试章节会细说。
第二件,事务控制。导游平台有典型的“下单 + 扣库存 + 生成关联记录”逻辑,这三步必须在一个事务里,否则订单失败时,库存状态不一致。实现方式是@Transactional注解不管用在哪一层,一定要保证事务边界在公开方法上。我习惯把事务加在controller还是service上?答案是service。Controller 只负责参数校验,事务在业务服务层统一控制,避免一个接口内部调用多个 service 方法时事务分裂。
第三件,MyBatis 缓存。MyBatis 有一级缓存(Session 级别)和二级缓存(Namespace 级别,可选开启)。在导游平台这类高并发查询场景里,很多人想开二级缓存省数据库压力,但我建议在项目早期不要开。原因是二级缓存默认是单机本地缓存,数据更新后缓存失效策略容易误伤;更关键的是,线上如果后面要拆微服务,本地缓存会变成脏数据来源。实际项目中我只有报表查询类的接口用了自定义 Redis 缓存,景点列表这种普通查询就让它直接走数据库,MySQL 自己也有查询缓存和索引优化,没必要在应用层加复杂度。
3.3 接口设计与常见“习惯性错误”的避让
后端接口是给前端用的“契约”,我设计接口时主要遵循三条约定:
- 统一返回格式。不管成功失败,都返回
{ code: 0, message: "", data: {} }这样的结构。前端在axios拦截器里统一处理code !== 0的情况,不用每个页面都写一遍错误判断。 - RESTful 语义化,但不强求。比如获取景点详情的路径用
GET /scenic/{id},创建订单用POST /order。小项目没必要纠结是/order/create还是POST /order,团队统一即可。 - 分页查询统一参数:
pageNum、pageSize。这个看着简单,但前后端最容易在“当前页是第几页”(pageNo还是pageNum)上扯皮。
后端“验参”也是常被忽略的点。前端虽然做了表单校验,但后端接口必须再做一遍。举个例子,创建订单接口的travelDate不能是过去日期,这个校验必须在后端处理,因为这是业务规则,不能依赖前端自觉。
4. 前端 Vue 实现核心要点
4.1 环境配置和三步检查法,绕开初始化报错
Vue 项目的环境配环境的话题能写一万字,这里只讲结论性操作。我的做法是,接手任何 Vue 项目第一步先看三样东西:
- Node 版本。Vue 2 项目建议 Node 14~16,Vue 3 项目建议 Node 16+。如果版本不匹配,最典型的现象是
npm run serve启动后页面白屏或者编译报错。 npm config get registry,确认镜像源可用。国内环境不配镜像,下载依赖的速度能把人等疯,更别谈安装成功率。node_modules是否存在以及是否有残留。项目从别的机器拷贝过来时,务必删掉node_modules后重新安装,避免平台相关的二进制文件冲突。
配好环境后,执行npm install,再npm run serve,如果端口没被占用,一般 5 分钟内能看到欢迎页。如果遇到sass或node-sass相关的安装报错,不用犹豫,把node-sass卸载换成sass(Dart Sass),兼容性好很多。本项目 Element UI 对sass的兼容是没有问题的,换了反而更省心。
4.2 路由设计:动态路由与权限控制的落地
导游平台有“游客浏览端”和“后台管理端”两套界面。最初做法是所有页面都写死在router/index.js里,后来发现一个问题:不同角色的用户能看到的功能不一样,管理员有“用户管理”“订单管理”,普通游客看到这些菜单毫无意义。
因此我在实际系统中使用了动态路由。简单说,后端登录接口返回当前用户的角色,前端根据角色字段去拼装菜单和路由。
const constantRoutes = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/home', children: [] } ] const asyncRoutes = { admin: [ { path: '/order', component: OrderList, meta: { title: '订单管理' } }, { path: '/guide', component: GuideManage, meta: { title: '导游管理' } } ], guide: [ { path: '/my-line', component: MyLine, meta: { title: '我的带团记录' } } ], user: [ { path: '/scenic', component: ScenicList, meta: { title: '景点浏览' } } ] }登录后根据role字段把对应数组concat到路由表,同时把菜单也根据这个数组渲染。动态路由的好处在于,前端不用把权限判断散落在各个页面的v-if里,菜单本身就是按角色生成的,从入口上规避了越权访问。当然,前端的路由控制只是体验层面的措施,真正权限校验一定要在后端接口上做,否则有人绕开浏览器直接调接口,白名单形同虚设。
4.3 数据请求模块封装,联调少走一半弯路
我见过太多 Vue 项目每个页面都写axios.get('http://localhost:8081/scenic/list'),结果后端一换端口、统一加个前缀,全项目搜索替换到崩溃。这个项目从一开始就收敛了一个统一的请求模块,核心内容如下:
import axios from 'axios' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || '/api', timeout: 10000 }) // 请求拦截器,统一带 token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers['Authorization'] = 'Bearer ' + token return config }) // 响应拦截器,统一处理业务错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { // 这里可以统一做 message 提示 return Promise.reject(new Error(res.message || 'Error')) } return res.data }, error => { // 401 时跳转登录页 if (error.response.status === 401) { localStorage.removeItem('token') location.href = '/login' } return Promise.reject(error) } ) export default service这里最关键的是baseURL。开发环境下 Vue CLI 会把/api开头的请求代理到后端地址,生产环境下 Nginx 也会把/api转发给后端 Java 服务。也就是说,baseURL永远不用变,环境差异全都交给代理层处理,这是前后端分离项目联调稳定性的命脉。没经历过“测试环境请求地址写死 localhost、上生产就白屏”的人,很难意识到这个细节的重要性。
5. 部署与发布:从源码到上线
5.1 前端打包与后端集成的两种路线
前后端分离项目的部署,行话叫“前端产物怎么喂给用户”。最常用的两种方式如下:
方案 A:前端打包后放入 SpringBoot 静态目录
npm run build生成dist目录,然后把整个目录拷贝到 SpringBoot 项目的src/main/resources/static下,重新mvn package。这样打出来的 Jar 包自带前端页面,部署时只需运行一个 Java 进程。
这种方式的优点是部署简单,尤其适合客户只有一个服务器、不熟悉 Nginx 的场景。缺点是违背了前后端分离的初衷——前端代码打包进了后端,以后前端要独立部署或做 CDN 加速,都得再改结构。该方案适合“给客户演示”和“内部小范围使用”的场景。
方案 B:前后端分离部署,用 Nginx 做静态服务
前端dist直接放到服务器 Nginx 的html目录,Nginx 负责访问静态页面,同时把所有/api开头的请求反向代理到 SpringBoot 端口。这是实际的“前后端分离部署”标准做法。
nginx.conf中关键的配置段:
server { listen 80; server_name your-domain.com; root /opt/gui-web/dist; index index.html; 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; } location /uploads/ { alias /opt/gui-web/uploads/; } }这里必须说明try_files $uri $uri/ /index.html;这行。Vue Router 默认是history模式,前端路由没有#,刷新页面时浏览器会拿着/order这种路径去请求服务器,Nginx 找不到对应文件就会 404。加了try_files后,找不到文件就统一回退到index.html,由前端路由接管路径。这是前后端分离部署里最容易漏、漏了必出 404 的经典问题。
生产上我推荐方案 B,因为 SpringBoot 用resourceHandler处理静态资源时,一旦需要上传图片和附件,路径解析经常出幺蛾子。独立 Nginx 直接通过alias把/uploads/映射到磁盘目录,底气足得多。
5.2 后端打包:Maven 那几步,为什么经常有人失败
后端打包命令很简单,mvn clean package -DskipTests。但有几个高频报错值得提前说。
第一,Maven 仓库里的依赖下载不全。换了台机器、网络不稳的时候,会出现 “Could not resolve dependencies” 或 “Failed to collect dependencies”。这种问题没有玄学,先确认 Maven 的settings.xml里镜像源是否可用,再确认依赖坐标是否打错。项目依赖尽量去 Maven 中央仓库或阿里云镜像拉,团队大了可以用 Nexus 做私服,这是提高构建成功率的根本。
第二,JDK 版本不一致。pom.xml里配的 Java 编译版本是 1.8,本地却用 JDK 17,打包可能能过,但运行期会出现UnsupportedClassVersionError。最省心的做法是让项目统一声明版本:
<properties> <java.version>1.8</java.version> </properties>然后 IDE 和服务器都安装对应 JDK。如果真的只能用更高版本 JDK,就干脆把这里改成 11 或 17,但要注意 SpringBoot 2.7 在 17 上跑也没有问题,主要是依赖兼容性要确认。
第三,打包完成后不要急着跑。检查一下target目录下生成的 Jar 包里,BOOT-INF/classes/mapper下是否有 XML 文件。如果 MyBatis 的 XML 没有被包含进 Jar,运行时会报 “Invalid bound statement”,这一般是pom.xml里没有配置资源扫描导致。解决办法是在pom.xml加:
<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build>如果嫌配置资源扫描太繁琐,可以把 Mapper XML 文件直接放到src/main/java下的同包目录里,但我不建议这么做,因为违反了资源与源码分离的习惯,后续代码审计和打包替换都不方便。
5.3 Tomcat 部署的特殊注意事项
除了用 SpringBoot 内置 Tomcat 打 Jar 包运行,还会遇到客户要求“必须部署到现有 Tomcat 的 webapps 目录”的情况。这时候要调整三处:
pom.xml中的<packaging>从jar改成war。- 启动类继承
SpringBootServletInitializer并重写configure()方法。 - 把 SpringBoot 内置 Tomcat 依赖设置为
provided,避免和外部 Tomcat 冲突。
这里要避坑的点是:如果前后端分离、前端已经独立部署到 Nginx,那么 WAR 部署到 Tomcat 时,后端接口的 Context Path 要单独配置。假设 Tomcat 中应用名为gui-server,那接口路径就会变成http://ip:8080/gui-server/api/...,Nginx 反向代理的目标地址也要同步带/gui-server,漏掉这一点,前端所有请求都会 404。
6. 常见问题与排查技巧实录
6.1 “Invalid bound statement (not found)” 的三种解法
这个报错在 SpringBoot + MyBatis 项目里出现概率极高,原因基本逃不出三种情况:
- Mapper 接口的
@MapperScan只扫描到了接口,XML 却没被加载。按 5.2 节的方法检查 Jar 包的资源和mapper-locations配置。 - XML 里的
namespace和接口全限定名不一致。比如接口是com.gltour.mapper.ScenicMapper,XML 写成了com.gltour.mapper.ScenicSpotMapper,直接报错。 - 接口方法名和 XML 里的
<select id="xxx">不一致,或者参数没写@Param。这个报的错往往不是直接的“not found”,而是“Parameter 'xxx' not found”,所以一旦出现这两种报错,先对一遍方法签名和 XML id。
这种问题的共性就是“接口与XML的绑定关系断了”,排查顺序建议:先看接口路径和包名,再看 XML namespace,最后看 xml 文件有没有被打包。按照这个顺序,一般 10 分钟内能定位。
6.2 跨域问题:前端请求发出去了,后端返回的却被浏览器拦截
前后端分离开发时,页面跑在localhost:8080,接口跑在localhost:8081,跨域问题几乎必然出现。前端浏览器控制台报 “Access to XMLHttpRequest ... has been blocked by CORS policy”,就是跨域。
解决办法我推荐在后端统一处理跨域,而不是在前端开代理后就不管了。前端vue.config.js的devServer.proxy只解决本地开发,上线前后端域名可能不一致,仍然需要后端允许跨域。最简单的做法是加一个全局 CORS 配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意这里我用的是allowedOriginPatterns("*"),如果使用allowedOrigins("*"),在allowCredentials(true)的情况下,会因浏览器安全策略而被拒绝。这也是一个经典坑。
6.3mysql ssl连接错误与Public Key Retrieval is not allowed
MySQL 8.0 的驱动默认行为导致两个非常高频的错误,这里一起说掉。
SSL connection error的解决方式已经提过,就是在 JDBC URL 上配置useSSL=false。报错信息里往往还有一句 “RSA public key is not available client side”,对应Public Key Retrieval is not allowed。这是因为 MySQL 8 的caching_sha2_password认证插件在 SSL 关闭情况下需要获取公钥,Java 驱动默认不允许。解决办法是在 JDBC URL 后面追加allowPublicKeyRetrieval=true:
jdbc:mysql://localhost:3306/gui_travel?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true到这里通常就能稳定连接了。如果你连的还是 5.7 的库,驱动也要相应换回com.mysql.jdbc.Driver和对应的 connector。版本匹配永远比参数折腾更优先。
6.4 打包部署之后前端白屏与 404 速查表
部署上线阶段,最高频的问题就是页面白屏、刷新 404、接口 502。我把常见的几种情况整理成一张速查表:
| 现象 | 可能原因 | 优先检查项 |
|---|---|---|
| 首页能打开,所有接口 404 | Nginx 的location /api/代理路径写错 | proxy_pass目标端口与后端实际端口 |
| 刷新任意子路径 404 | Nginx 缺少try_files回退 | 确认location /中try_files $uri $uri/ /index.html |
| 接口报 502 | 后端服务挂了或防火墙拦截 | 用curl直接访问后端端口,区分 Nginx 还是 Java 问题 |
| 上传图片无法显示 | 静态资源路径不匹配 | Nginx 的alias目录与实际存放目录 |
| 登录后页面跳转不生效 | 路由模式是hash,前端鉴权拦截错误 | 检查router.beforeEach的next()逻辑 |
再提一个容易被忽略的点:前端vue.config.js中配置了publicPath: './',打包后资源是相对路径,配合某些 Nginx 的二级路径部署时很省事,但如果直接用根路径部署,建议把publicPath设为/。这是一个会造成“能打开页面但样式全不见”的隐藏坑,遇到页面无样式问题时第一个就该想到它。
6.5 事务不生效的排查方向
导游平台的下单接口涉及多张表写入,经常出现“订单生成了,状态却变成了已取消”这种问题。如果代码里加了@Transactional,但排查下来事务没生效,两个常见方向需要优先确认:
- 同类内部调用。A 方法调用同类中 B 方法,B 上有
@Transactional,此时事务不生效,因为 Spring AOP 的代理机制只拦截外部调用,内部自调用不走代理。解决办法是拆成不同类,或者把事务注解往上层放。 - 异常吃掉了。
@Transactional默认只在RuntimeException(非受检异常)时回滚。如果 catch 了异常没有抛出,或者没加rollbackFor = Exception.class,那么受检异常下事务不会回滚。对应的代码风格是:异常处理统一放 Controller 层,Service 层抛出业务异常,让事务自然回滚。
7. 最后分享几个实操体会
项目做完后回看整个流程,我最想说的是:不要迷信技术,也不要轻视工程化。前后端分离只是手段,不是为了看起来高级。实际做的时候,把接口约定、目录结构、配置文件这些底子打好,开发效率自然就上来了。
部署方面,强烈建议每套环境(开发、测试、生产)都准备一份独立的配置文件,通过 SpringBoot 的application-{profile}.yml做环境隔离。我第一次部署生产环境时直接用了开发环境的配置,结果连的是本地数据库,差点把测试数据冲掉,从那以后环境隔离成为红线。
另外,导游平台这类项目,交付给客户后常被要求加“节假日流量大时系统不卡”。在没有负载均衡硬件的情况下,最有效的手段就是给热点接口加 HTTP 缓存头、给数据库高频查询字段建索引、前端静态资源上 CDN。这一套做下来,单机 Tomcat 也能扛住相当可观的流量。
后端用 SpringBoot 记得把端口、数据库密码、上传路径从代码里彻底剥离出来,用环境变量或者jasypt加密。不要问为什么,曾经把明文数据库密码写在application.yml里提交到 Git 仓库,后面被扫出漏洞的教训太深了。一个行业老兵能给你的建议就是:密码不进仓库,环境配置永远独立。
这个项目做到后期,我最大的感受是“武功再高,也怕菜刀”——架构选型再牛,部署脚本、数据库脚本、README 这些看得见摸得着的东西,才是客户和团队最依赖的。写文档和配环境从来不是简单的小事,它是项目能不能真正落地的那道槛。