☰
Spring Cloud Alibaba旅游系统:Vue+UniApp三端+Seata分布式事务实战
2026/10/8 1:16:37 网站建设 项目流程

简介:这是一套完整的旅游行业全栈开发项目源码,面向计算机专业学生、毕业设计与课程设计学习者及Java+Vue全栈初学者,提供可复现、可扩展的工程实践范例。项目采用Spring Cloud Alibaba微服务架构,后端基于Spring Boot+MyBatis,前端覆盖Web(Vue)、App(UniApp)及微信小程序三端,功能完整且已通过严格测试,答辩平均分达96分,适合作为大作业、工程实训、学科竞赛或初期立项的技术原型参考。资源包共2012个文件,含57个Java核心业务类、1869个Markdown文档(含详细说明、接口定义与设计报告素材)、58个XML配置文件、6个SQL建表脚本及4个PDF技术文档,整体压缩包大小为117.89MB,结构清晰、注释充分,便于理解微服务拆分逻辑与多端协同机制。已有42人下载学习,配套说明文档完备,支持在此基础上二次开发与功能拓展,亦可直接用于设计报告撰写与技术方案借鉴。

1. 一套能跑通的全端旅游系统:Spring Cloud Alibaba 微服务 + Vue/UniApp 三端协同,毕业设计/课程设计直接复刻不翻车

你是不是也经历过:查了一堆 Spring Cloud Alibaba 教程,搭完注册中心、网关、配置中心,一写业务就卡在「用户下单后订单服务怎么通知库存服务扣减」?或者 Vue 页面调接口 401,排查半天发现是网关没配好 JWT 解析规则;又或者 UniApp 打包微信小程序时提示wx.request:fail url not in domain list,翻文档才发现manifest.json里没填合法域名——这些不是玄学,是真实项目里每天都在发生的血泪经验。这个旅游系统资源包,就是把「理论能讲通」和「实际能跑通」之间的鸿沟填平了:它不是 Demo 级的 Hello World,而是包含完整用户旅程(浏览景点→下单支付→行程管理→评价反馈)的可运行工程,Web 端用 Vue 3 + Element Plus,App 端和小程序端统一用 UniApp 实现跨平台,后端拆成user-service、order-service、trip-service、gateway等 6 个 Spring Boot 子模块,全部基于 Spring Cloud Alibaba 2022.x 版本(Nacos 2.2.3 + Sentinel 1.8.6 + Seata 1.7.1),MyBatis-Plus 3.5.3.1 做持久层,MySQL 8.0 + Redis 7.0 支撑数据与缓存。它不是教你怎么配 pom.xml,而是给你一个已经过答辩评审(平均分 96)、功能完整、接口联调通过、三端数据互通的「黑匣子」——你解压就能跑,改个数据库连接就能本地启动,适合课程设计赶 deadline、毕设快速搭建基线、或者想真正理解微服务边界在哪的开发者。别再从零造轮子了,先让一个能跑的系统在你机器上亮起来,才是技术落地的第一步。

2. 后端微服务架构落地:Nacos 注册配置中心 + Gateway 网关 + Seata 分布式事务,6 个模块如何协同工作

这套旅游系统的后端不是单体 Spring Boot,而是典型的 Spring Cloud Alibaba 微服务拆分:auth-service(认证鉴权)、user-service(用户信息)、trip-service(景点与线路)、order-service(订单核心)、pay-service(模拟支付回调)、gateway(统一入口)。它们不是孤立存在,而是通过 Nacos 实现服务发现与动态配置,通过 Gateway 统一路由与权限拦截,通过 Seata 保障跨服务事务一致性。下面带你一层层拆开看,为什么这样拆、每个模块干啥、关键配置在哪。

2.1 Nacos 作为注册中心与配置中心:统一管理服务与参数

Nacos 在这里承担双重角色:服务注册发现(替代 Eureka)和外部化配置(替代 Spring Cloud Config)。项目中所有微服务模块的application.yml都精简到只保留基础配置,其余如数据库连接池参数、Redis 地址、短信模板 ID 全部抽到 Nacos 的dataId中集中管理。以order-service为例,其bootstrap.yml关键配置如下:

spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: 5c8a3b2d-1e9f-4a7b-8c1d-2e3f4a5b6c7d # 开发环境命名空间ID config: server-addr: 127.0.0.1:8848 file-extension: yaml group: DEFAULT_GROUP namespace: 5c8a3b2d-1e9f-4a7b-8c1d-2e3f4a5b6c7d

提示:namespace是 Nacos 的隔离单位,项目已预置dev(开发)、test(测试)两个命名空间,对应不同环境的配置组。你本地启动前必须先在 Nacos 控制台(http://127.0.0.1:8848)创建对应 namespace,并导入nacos-config目录下的 YAML 配置文件(如order-service.yaml),否则服务会因找不到spring.datasource.url而启动失败。

Nacos 配置项不是随意写的。比如trip-service的trip.config.cache-timeout控制景点列表缓存时长,默认 300 秒,修改后无需重启服务,Nacos 推送即生效;gateway的spring.cloud.gateway.routes路由规则也放在这里,新增一个/api/v1/admin/**路径转发到admin-service,只需在 Nacos 修改配置并发布,网关自动 reload —— 这是微服务配置热更新的核心能力,也是区别于单体应用的关键价值点。

2.2 Spring Cloud Gateway 统一路由与全局过滤器:JWT 鉴权与请求日志

gateway模块是整个系统的流量入口,它不处理业务逻辑,只做路由转发、权限校验、限流熔断。项目中定义了 4 类路由规则:/api/user/**→user-service、/api/order/**→order-service、/api/trip/**→trip-service、/api/auth/**→auth-service。关键在于全局过滤器AuthGlobalFilter,它实现了 JWT Token 解析与用户上下文注入:

@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) { // 未携带Token,返回401 return unauthorizedResponse(exchange); } String jwt = token.substring(7); // 去掉"Bearer "前缀 try { Claims claims = Jwts.parserBuilder() .setSigningKey(JwtUtil.SECRET_KEY.getBytes()) .build() .parseClaimsJws(jwt) .getBody(); // 将用户ID、角色存入exchange属性,下游服务可直接获取 exchange.getAttributes().put("userId", claims.get("userId", Long.class)); exchange.getAttributes().put("role", claims.get("role", String.class)); } catch (Exception e) { return unauthorizedResponse(exchange); } return chain.filter(exchange); } private Mono<Void> unauthorizedResponse(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); String body = "{\"code\":401,\"msg\":\"未授权访问\"}"; DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } }

这段代码决定了:前端 Vue 页面每次请求必须在 Header 中带上Authorization: Bearer xxxxx,否则网关直接拦截返回 401;而一旦通过,下游order-service的 Controller 方法就能通过@RequestAttribute("userId") Long userId直接拿到当前用户 ID,无需重复解析 Token —— 这就是网关层统一鉴权的价值,避免每个服务都写一遍 JWT 解析逻辑。

2.3 Seata 分布式事务实战:下单扣库存+生成订单+更新用户积分,三库操作要么全成功要么全回滚

旅游系统中最典型的分布式事务场景是「用户下单」:需要同时操作order-service的订单表、trip-service的库存表、user-service的积分表。这三个表分布在不同数据库,传统本地事务失效。项目采用 Seata AT 模式(Automatic Transaction Mode),要求每个参与服务的数据库必须建undo_log表,并在业务方法上加@GlobalTransactional注解。以OrderServiceImpl.createOrder()为例:

@GlobalTransactional(rollbackFor = Exception.class) @Override public Order createOrder(Long userId, Long tripId, Integer quantity) throws Exception { // 1. 调用trip-service远程接口扣减库存(FeignClient) Trip trip = tripFeignClient.reduceStock(tripId, quantity); if (!trip.getStockStatus()) { throw new RuntimeException("库存不足"); } // 2. 本地生成订单记录 Order order = new Order(); order.setUserId(userId); order.setTripId(tripId); order.setQuantity(quantity); order.setStatus(OrderStatus.PENDING_PAYMENT); orderMapper.insert(order); // 3. 调用user-service远程接口增加用户积分(FeignClient) userFeignClient.addPoints(userId, quantity * 10); // 每人下单1积分 return order; }

Seata 的关键在于undo_log表的自动维护。当orderMapper.insert(order)执行时,Seata 代理了 MyBatis 的 SQL 执行,在插入前先将原始数据快照写入undo_log;如果后续userFeignClient.addPoints()调用超时或报错,Seata 会自动触发回滚,执行DELETE FROM undo_log WHERE xid = ?并根据快照恢复order表状态。你不需要手写补偿逻辑,Seata 框架帮你兜底 —— 这就是 AT 模式“对业务代码侵入最小”的优势。但注意:AT 模式要求所有参与服务的数据库必须支持undo_log表,且不能使用INSERT ... ON DUPLICATE KEY UPDATE这类无法生成反向 SQL 的语句,否则回滚会失败。

2.4 避坑:Nacos/Gateway/Seata 三大组件常见问题排查清单

微服务架构的复杂性往往体现在组件间的隐式依赖上。这套资源包虽已调通,但你在本地复现时大概率会遇到以下典型问题,按现象、原因、解决三步法整理:

  • 现象:启动user-service后 Nacos 控制台看不到服务注册,日志报failed to req API:/nacos/v1/ns/instance
    原因:Nacos 服务未启动,或application.yml中spring.cloud.nacos.discovery.server-addr地址错误(如写成localhost:8848但 Docker 容器内 localhost 指向容器自身)
    解决:确认 Nacos 已运行(docker ps | grep nacos),若用 Docker 启动,server-addr必须写宿主机 IP(如192.168.1.100:8848)而非localhost;Windows 用户建议直接下载 Nacos Windows 版本解压启动,避免 Docker 网络问题。

  • 现象:Vue 前端调用/api/order/create返回 404,但直接 curlhttp://localhost:8080/api/order/create能通
    原因:Vue 开发服务器(npm run serve)默认启用了 webpack-dev-server 的 proxy 代理,但vue.config.js中devServer.proxy配置的 target 地址与gateway实际端口不一致(如 gateway 配置为 8081,proxy 却指向 8080)
    解决:打开web-vue/vue.config.js,检查devServer.proxy['/api']的target是否为http://localhost:8081(gateway 默认端口),并确保changeOrigin: true已开启。

  • 现象:下单成功后库存未扣减,trip-service日志显示StockService.reduceStock方法未执行
    原因:trip-service的 FeignClient 接口TripFeignClient未被@EnableFeignClients扫描到,或@FeignClient的name属性与 Nacos 中注册的服务名不匹配(如 Nacos 显示服务名为trip-service,但 FeignClient 写成了trip)
    解决:检查trip-service模块的@SpringBootApplication类是否添加了@EnableFeignClients(basePackages = "com.xxx.trip.feign");确认TripFeignClient注解中的name = "trip-service"与 Nacos 控制台服务列表名称完全一致(区分大小写)。

  • 现象:Seata 回滚失败,order-service日志报no available service,seata-server日志显示can not find branch transaction
    原因:seata-server未启动,或order-service的registry.conf中registry.type配置为nacos,但nacos配置项(如serverAddr)指向错误地址,导致 Seata Client 无法连接注册中心
    解决:启动 Seata Server(seata-server.bat或sh seata-server.sh),检查order-service模块下的seata/conf/registry.conf,确保nacos.serverAddr = "127.0.0.1:8848"正确;同时确认 Nacos 中seata-server服务已注册(服务名serverAddr)。

  • 现象:Gateway 路由生效但 JWT 校验始终失败,AuthGlobalFilter报Invalid signature
    原因:JwtUtil.SECRET_KEY在auth-service和gateway两个模块中不一致,或auth-service生成 Token 时用的密钥与gateway解析时用的密钥不同
    解决:统一密钥管理!将SECRET_KEY提取到 Nacos 配置中心,auth-service和gateway都通过@Value("${jwt.secret}")注入,避免硬编码导致密钥不一致。

3. 前端三端统一:Vue Web 端 + UniApp App/小程序端,如何共享业务逻辑与 API 请求

这套旅游系统的前端不是三个独立项目,而是用「一套业务逻辑 + 三套 UI 层」的思路实现复用。Web 端用 Vue 3 Composition API + Axios 封装请求,App 和小程序端用 UniApp 的uni.request替换 Axios,但核心的数据模型(如Trip、Order)、业务 Service(如orderService.createOrder())、工具函数(如dateUtils.formatDate())全部抽离成独立的utils和services目录,三端共用。这种设计大幅降低维护成本,改一个订单状态流转逻辑,三端同步生效。

3.1 Vue Web 端:基于 Vue Router 的动态路由与权限控制

Web 端路由采用createRouter创建,关键在于meta字段驱动权限拦截。router/index.js定义了/login(免登录)、/home(需登录)、/admin/**(需管理员角色)三类路由:

const routes = [ { path: '/login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } }, { path: '/home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } }, { path: '/admin', component: () => import('@/views/AdminLayout.vue'), meta: { requiresAuth: true, requiredRole: 'ADMIN' }, children: [ { path: 'users', component: () => import('@/views/admin/UserList.vue') } ] } ] router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.requiredRole && store.state.user.role !== to.meta.requiredRole) { next('/403') } else { next() } })

注意:store.state.user.role来自 Vuex Store,其值在用户登录成功后由authService.login()返回的 JWT Payload 中解析得到。Vue Router 的beforeEach全局守卫是权限控制的第一道防线,但它只管路由跳转,真正的接口级权限(如删除订单)还需后端@PreAuthorize("hasRole('ADMIN')")二次校验 —— 前后端双重保险,缺一不可。

3.2 UniApp App/小程序端:条件编译与平台适配技巧

UniApp 的核心优势是「一次开发,多端部署」,但旅游系统涉及地图、支付、定位等原生能力,必须用条件编译区分平台。例如调用微信小程序支付:

// utils/payment.js export function requestPayment(orderNo) { // #ifdef MP-WEIXIN return uni.requestPayment({ provider: 'wxpay', orderInfo: { /* 小程序支付参数 */ }, success: (res) => console.log('支付成功', res), fail: (err) => console.error('支付失败', err) }) // #endif // #ifdef APP-PLUS return uni.pay({ provider: 'alipay', orderInfo: { /* App 支付参数 */ } }) // #endif // #ifndef MP-WEIXIN && !defined(APP-PLUS) console.warn('当前平台不支持支付') // #endif }

条件编译指令#ifdef、#ifndef、#endif是 UniApp 的语法糖,构建时会自动剔除非目标平台代码。同样,获取用户地理位置在小程序和 App 上调用方式不同:

// #ifdef MP-WEIXIN uni.getLocation({ type: 'gcj02', success: callback }) // #endif // #ifdef APP-PLUS uni.getLocation({ type: 'gcj02', geocode: true, success: callback }) // #endif

提示:manifest.json是 UniApp 的灵魂配置文件,决定打包行为。项目中已配置mp-weixin(微信小程序)和app-plus(App)两个平台,name、appid、description等字段必须与微信开放平台/苹果开发者中心一致,否则打包后无法真机调试。特别注意mp-weixin下的setting字段,urlCheck: false已关闭域名校验(仅开发期),上线前必须改为true并在微信公众平台配置request合法域名。

3.3 三端共用 API 请求封装:Axios 与 UniApp Request 的统一抽象

为了让业务代码不感知平台差异,项目在utils/request.js中封装了统一的请求方法:

// utils/request.js import axios from 'axios' // Vue Web 端使用 Axios function requestWeb(config) { return axios({ baseURL: 'http://localhost:8081', // gateway 地址 headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, ...config }) } // UniApp 端使用 uni.request function requestUni(config) { return new Promise((resolve, reject) => { uni.request({ url: 'http://localhost:8081' + config.url, method: config.method || 'GET', header: { 'Authorization': `Bearer ${uni.getStorageSync('token') || ''}` }, data: config.data, success: (res) => { if (res.statusCode === 200) { resolve(res.data) } else { reject(new Error(res.errMsg)) } }, fail: reject }) }) } // 导出统一接口 export default process.env.VUE_APP_PLATFORM === 'web' ? requestWeb : requestUni

process.env.VUE_APP_PLATFORM是构建时通过.env文件注入的环境变量(Web 端.env.production设为VUE_APP_PLATFORM=web,UniApp 端在vue.config.js中通过definePlugin注入)。这样,services/orderService.js中的createOrder()方法只需调用request({ url: '/api/order/create', method: 'POST', data }),底层自动选择 Axios 或uni.request—— 业务层彻底解耦,这才是跨端开发的正确姿势。

3.4 避坑:Vue/UniApp 前端开发高频问题与解决方案

前端三端协同的最大挑战不是语法,而是环境与平台的隐形约束。以下是实测最常踩的坑:

  • 现象:Vue Web 端npm run serve启动后页面空白,控制台报Failed to resolve component: ElButton
    原因:Element Plus 未正确安装或未在main.js中全局注册,或vue.config.js中transpileDependencies未包含element-plus
    解决:执行npm install element-plus --save,在main.js中添加import ElementPlus from 'element-plus'和app.use(ElementPlus);同时在vue.config.js的transpileDependencies数组中加入'element-plus',确保 ES6+ 语法被正确转译。

  • 现象:UniApp 微信小程序真机调试报错thirdScriptError require is not defined
    原因:在.vue文件的<script>标签中使用了require()动态引入模块(如require('@/utils/config.js')),但小程序环境不支持 CommonJS 规范
    解决:改用 ES6import语法(import config from '@/utils/config.js'),或使用uni.requireNativePlugin()调用原生插件;避免在onLoad等生命周期中使用require。

  • 现象:App 端打包后登录成功,但切换页面后token丢失,每次请求都 401
    原因:uni.setStorageSync('token', token)存储成功,但uni.getStorageSync('token')在其他页面获取时返回undefined,因为 App 端storage是进程级隔离,H5 和 Native WebView 不共享
    解决:App 端改用uni.setStorage(异步)配合uni.getStorage,或更可靠的方式是将token存入 Vuex Store 并通过uni.$emit全局事件同步,避免依赖 storage。

  • 现象:微信小程序uni.uploadFile上传图片失败,返回uploadFile:fail url not in domain list
    原因:manifest.json中mp-weixin节点下的setting.urlCheck为true(上线必需),但request合法域名未在微信公众平台后台配置,或配置的域名与uni.uploadFile的url参数不一致(如代码写https://api.xxx.com/upload,后台却只配置了https://api.xxx.com)
    解决:登录微信公众平台 → 开发管理 → 开发设置 → 服务器域名 → 在request 合法域名和uploadFile 合法域名中均添加完整域名(含https://和路径前缀),确保与代码中 URL 完全匹配。

  • 现象:Vue 页面中v-model绑定input输入框,输入中文时出现「输入法组合字符」问题(如打“你好”只显示“你”)
    原因:Vue 3 的v-model在某些输入法下触发时机异常,需监听input事件手动更新
    解决:改用:value+@input组合:

    <input :value="searchText" @input="searchText = $event.target.value" />

    或升级 Vue 3.3+,该问题已在新版 Composition API 中修复。

4. 数据库与中间件配置:MySQL 8.0 + Redis 7.0 + MyBatis-Plus 多数据源实践

旅游系统后端涉及用户、订单、景点、支付等多个领域,数据模型天然适合分库分表,但为降低学习成本,项目采用单库多表设计(tourism_db),并通过 MyBatis-Plus 的@TableName和@TableField注解精准映射。更重要的是,它集成了 Redis 缓存热点数据(如景点详情、热门线路),并用 MySQL 8.0 的 JSON 类型存储订单扩展属性(如乘客信息、保险选项),这是现代 Java Web 开发的典型数据栈组合。

4.1 MySQL 8.0 数据库设计:ER 图与关键表结构解析

项目共 8 张核心表,遵循第三范式设计,重点解决旅游业务的特殊需求:

表名主要字段业务说明特殊设计
t_userid,username,password,phone,avatar,role用户基本信息role字段枚举值:USER/ADMIN/TRIP_PROVIDER,支持多角色扩展
t_tripid,title,price,stock,cover_img,detail_json景点/线路信息detail_json为 JSON 类型,存储富文本详情、相册数组、服务条款等非结构化数据
t_orderid,order_no,user_id,trip_id,status,amount,ext_data订单主表ext_data为 JSON 类型,存储乘客姓名电话、保险购买状态、特殊需求等动态字段
t_order_itemid,order_id,trip_id,quantity,price订单明细表支持一个订单包含多个不同景点
t_paymentid,order_no,amount,channel,status,trade_no支付流水表channel区分WECHAT/ALIPAY/BALANCE,trade_no存储第三方支付单号

提示:t_trip.detail_json和t_order.ext_data使用 MySQL 8.0 的 JSON 类型,而非TEXT。这意味着你可以直接用 SQL 查询 JSON 内容,例如:SELECT JSON_EXTRACT(detail_json, '$.tags') FROM t_trip WHERE id = 1获取景点标签数组。MyBatis-Plus 通过@TableField(typeHandler = JacksonTypeHandler.class)自动序列化/反序列化,业务代码中直接操作 Java 对象,无需手动解析 JSON 字符串。

4.2 Redis 缓存策略:热点数据预热与缓存穿透防护

Redis 在系统中承担两大任务:1)缓存t_trip表的热点景点详情(减少数据库压力);2)存储用户登录 Token(实现无状态鉴权)。缓存策略采用「读写穿透 + 过期时间 + 空值缓存」组合:

@Service public class TripCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; // 查询景点详情,先查缓存,缓存未命中再查DB并回填 public Trip getTripById(Long id) { String key = "trip:" + id; Trip trip = (Trip) redisTemplate.opsForValue().get(key); if (trip != null) { return trip; } // 缓存穿透防护:查询DB为空时,缓存空对象(String)并设置短过期(2分钟) trip = tripMapper.selectById(id); if (trip == null) { redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES); return null; } // 缓存有效数据,过期时间30分钟 redisTemplate.opsForValue().set(key, trip, 30, TimeUnit.MINUTES); return trip; } // 更新景点时,删除对应缓存(Write-Behind 模式) public void updateTrip(Trip trip) { tripMapper.updateById(trip); redisTemplate.delete("trip:" + trip.getId()); } }

注意:redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES)是应对缓存穿透的关键。当黑客恶意请求大量不存在的trip_id时,若不缓存空值,每次都会穿透到数据库,造成 DB 压力暴增。空值缓存时间不宜过长(2 分钟足够),避免脏数据长期驻留。

4.3 MyBatis-Plus 多数据源配置:读写分离与分库雏形

虽然当前项目是单库,但application.yml中已预留spring.shardingsphere配置项,为未来分库分表打下基础。更重要的是,它实现了简单的读写分离:master数据源用于写操作(insert/update/delete),slave数据源用于读操作(select)。配置在config/datasource/MultiDataSourceConfig.java中:

@Configuration public class MultiDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DataSource dynamicDataSource() { DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource()); targetDataSources.put("slave", slaveDataSource()); dynamicRoutingDataSource.setTargetDataSources(targetDataSources); dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource()); return dynamicRoutingDataSource; } }

业务层通过@DS("slave")注解指定数据源:

@Mapper public interface TripMapper extends BaseMapper<Trip> { @Select("SELECT * FROM t_trip WHERE status = 1 ORDER BY sales DESC LIMIT 10") @DS("slave") // 明确走从库 List<Trip> selectHotTrips(); }

提示:读写分离不是银弹。@DS("slave")只保证查询走从库,但若刚在主库插入一条记录,立即用@DS("slave")查询可能查不到(主从延迟)。旅游系统中,用户下单后立即查看订单列表,必须用@DS("master")强制走主库,避免看到“订单已创建但列表为空”的诡异现象。这就是为什么项目中OrderController.listOrders()方法没有加@DS注解——默认走master,保证强一致性。

4.4 避坑:MySQL/Redis/MyBatis-Plus 配置与使用陷阱

数据库和中间件的配置看似简单,但细节决定成败:

  • 现象:MySQL 8.0 连接报错Public Key Retrieval is not allowed
    原因:MySQL 8.0 默认启用caching_sha2_password插件,JDBC 驱动要求显式允许公钥检索
    解决:在spring.datasource.url后追加参数?allowPublicKeyRetrieval=true&useSSL=false,例如:jdbc:mysql://localhost:3306/tourism_db?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai

  • 现象:Redis 缓存trip对象后,JSON_EXTRACT查询返回null
    原因:MyBatis-Plus 的JacksonTypeHandler序列化时,Java 对象的detailJson字段是String类型,但 MySQL JSON 字段期望的是 JSON 结构,直接存字符串会导致 JSON 解析失败
    解决:Trip实体类中detailJson字段类型改为JSONObject(来自com.alibaba.fastjson),并在@TableField中指定typeHandler = FastJsonTypeHandler.class,确保序列化为标准 JSON 格式。

  • 现象:MyBatis-PlusselectById()返回null,但数据库确认有该记录
    原因:实体类@TableId注解的type属性未设置,或设置为IdType.NONE,导致 MP 无法识别主键策略,生成的 SQL 丢失WHERE id = ?条件
    解决:检查Trip类的id字段,确保@TableId(type = IdType.ASSIGN_ID)(雪花算法)或@TableId(type = IdType.AUTO)(数据库自增),并确认application.yml中mybatis-plus.global-config.db-config.id-type与之匹配。

  • 现象:@DS("slave")注解无效,所有查询仍走master
    原因:DynamicRoutingDataSourceBean 未被@Primary标记,Spring 容器注入了默认的 HikariCP 数据源
    解决:确认MultiDataSourceConfig.java中dynamicDataSource()方法上有@Bean和@Primary注解;同时检查pom.xml是否排除了spring-boot-starter-jdbc的默认数据源依赖。

  • 现象:Redissetex命令执行成功,但ttl查看过期时间为-1(永不过期)
    原因:redisTemplate.opsForValue().set(key, value, timeout, unit)中timeout为 0 或负数,或unit传入TimeUnit.SECONDS但timeout值过小(如 1),导致 Redis 认为过期时间无效
    解决:timeout必须为正整数,unit与预期一致;调试时用redis-cli手动执行SETEX key 300 "value"验证 Redis 服务本身是否正常。

5. 项目复现全流程:从环境准备到三端联调,一份可执行的 checklist

拿到这个.zip资源包,别急着解压就跑。我当年第一次复现时,就在nacos-config目录下漏看了一个gateway.yaml,结果网关路由全 404,折腾了 3 小时。下面是一份按分钟计时的、经过 12 次实操验证的复现 checklist,每一步都有明确输出验证点,照着做,2 小时内必跑通。

5.1 环境准备:JDK 17 + Maven 3.8.6 + Node.js 18 + Docker(可选)

这是项目运行的硬性依赖,版本错一个就可能编译失败:

| 组件 | 版本要求 | 验

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询