
简介面向旅游项目开发与学习者提供一套覆盖 Web 端、App 端与微信小程序端的全栈工程源码。技术选型以 Spring Cloud Alibaba 微服务体系为骨架后端采用 Spring Boot 与 MyBatis 实现业务接口与数据持久化前端使用 Vue 和 UniApp 完成多端页面展示整体架构适用于毕业设计、课程设计、工程实训以及微服务方向的项目练手。压缩包大小约一百一十八兆共包含两千余个文件其中大量 Markdown 文档用于说明环境搭建、模块设计、运行流程等Java 文件承载核心业务SQL 脚本负责数据库初始化XML 与 Properties 用于服务配置另有 PDF 设计材料可供参考内容按前端、后端、小程序分模块组织便于对照阅读和部署也适合在现有基础上扩展更多旅游业务功能。项目关键功能已经过测试运行答辩评审平均分达到九十六分能够按说明完成复现。目前已有四十二人学习下载符合需要可运行项目与工程参考的人群。1. 旅游业务三端一套代码这套技术栈解决什么适合什么人接手一个旅游平台的真实形态往往是这样的运营后台在 Web 端做订单管理、线路上下架游客用 App 刷攻略、下单、支付微信小程序承接分享裂变和轻量预订。三端共用同一套业务规则但如果后台、客户端、小程序各自为政开发成本会直接翻倍。这套以 Spring Cloud Alibaba 相关组件加 Spring Boot、MyBatis 做后端Vue 做 Web 端UniApp 同时出 App 和小程序的工程就是冲着「一套规则、三端复用」去的。接手这个项目的人一般是有 Java 基础、想摸清微服务拆分和多端打包的从业者。它让你看清后端微服务粒度切到哪一层合适也能让你在同一个仓库里把 Web 管理端和 C 端小程序都跑起来。2. 后端微服务骨架Spring Cloud Alibaba 组件选型与服务划分2.1 旅游业务为什么要拆微服务从单体到 Nacos 注册中心的改造理由旅游项目表面是卖门票和线路实际业务链路很长。用户登录、线路搜索、库存扣减、订单生成、支付回调、退款审核每一步都可能独立扩容。比如节假日门票秒杀时订单服务压力大但攻略内容服务几乎没流量单体应用只能整包扩容浪费资源。拆成微服务后订单服务单独开三副本其他服务保持单副本资源利用率天差地别。Spring Cloud Alibaba 在这一套里承担的就是微服务底座。Nacos 做注册中心和配置中心Sentinel 做流量控制和熔断Seata 做分布式事务OpenFeign 做服务间调用。我一般会按业务域拆成用户服务、订单服务、产品服务、支付服务四个基础服务再加一个网关模块统一入口。网关用 Spring Cloud Gateway路由规则指向 Nacos 里的服务名前端只管调网关不需要知道后端哪个服务在处理。服务拆完以后第一个要改的就是数据源。拆服务不能拆库拆出分布式事务灾难旅游项目里订单、产品、用户数据天然分开订单服务和产品服务各用各的库即可。真正麻烦的是支付回调同时要改订单状态和库存这种跨服务事务用 Seata 的 AT 模式最省事业务代码里只要加一个GlobalTransactional注解回滚逻辑由 Seata 自动处理。2.2 用 Spring Boot MyBatis 搭一个可运行的订单服务最小工程与配置后端最小的可运行工程不算复杂。pom.xml 里引入 spring-boot-starter-web、mybatis-spring-boot-starter、spring-cloud-starter-alibaba-nacos-discovery 三组依赖加一个订单表和一个订单 Mapper就能把服务跑起来注册到 Nacos。要注意的是 Spring Boot 版本别追新Spring Cloud Alibaba 每个版本对应固定的 Spring Boot 版本版本对不上会出现各种奇怪的 Bean 找不到问题。用户搜索里频繁出现「springboot版本太高」的坑这里重点说。我常用的组合是 Spring Boot 2.7.x 对应 Spring Cloud Alibaba 2021.0.5.0这个组合经过大量生产验证。Spring Boot 3.x 的 Jakarta 命名空间改动会让老版 MyBatis 和 Sentinel 直接失配如果不是全新项目没必要冒险。pom 里显式锁定版本号不要用RELEASE这种浮动版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties spring-cloud-alibaba.version2021.0.5.0/spring-cloud-alibaba.version /properties dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies这段依赖里Nacos 的 discovery 负责服务注册发现config 负责拉取远程配置。MyBatis 选 2.3.2 是因为它兼容 Spring Boot 2.7不会像新版那样要求 Spring Boot 3。配置文件里要把 Nacos 地址、命名空间、数据库连接都写清楚Nacos 地址写错的话服务启动后一直报连接拒绝日志里会反复出现Client not connected。server: port: 8081 spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: travel-dev config: server-addr: 127.0.0.1:8848 file-extension: yaml datasource: url: jdbc:mysql://127.0.0.1:3306/travel_order?useUnicodetruecharacterEncodingutf8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这里的map-underscore-to-camel-case我建议直接设为 true数据库字段user_id自动映射到实体类userId少写一堆resultMap。旅游项目的订单表会有order_no、product_id、total_amount这类字段开这个配置后实体类不用加注解也可以正常映射。启动类上加MapperScan(com.travel.order.mapper)扫描 Mapper 接口工程就能跑起来了。2.3 Spring Cloud Alibaba 三个必调参数Nacos 地址、Sentinel 熔断阈值、Seata 事务超时很多人在微服务跑通后就以为完事了实际上有三个参数不调生产环境迟早出问题。第一个是 Nacos 的命名空间和环境隔离开发、测试、生产用同一个 Nacos 实例时不按命名空间隔离会让配置互相覆盖。我会在 Nacos 控制台建三个命名空间每个服务的配置文件里把namespace指定清楚。第二个是 Sentinel 的熔断阈值。旅游项目有明显的流量峰谷五一、国庆期间订单接口流量是平时的几十倍。Sentinel 默认阈值是单机 10 QPS这个值对订单服务来说太保守。我的做法是先压测观察订单查询接口的合理响应时间把熔断阈值设在压测峰值的 1.5 倍左右。阈值设太低会导致误伤正常流量设太高熔断机制形同虚设。参数推荐值说明Nacos namespace按环境隔离避免配置串环境Sentinel 熔断阈值压测峰值的 1.5 倍设太低误伤正常流量Seata 全局事务超时30 秒旅游支付链路含外部回调第三个是 Seata 的全局事务超时。订单支付成功后要扣减产品库存、更新订单状态、调用营销服务发积分这一条链路涉及三个服务。默认 60 秒超时在慢接口场景下会拖住数据库连接池我一般调到 30 秒超过就回滚并记录异常订单号。这三个参数调完微服务骨架才算真正可用。3. Web 端用 Vue 把后台管理撑起来工程搭建与关键页面3.1 用 Vite 创建 Vue 3 工程与依赖安装的取舍Web 端在这个项目里主要承担运营后台的职责产品列表、订单管理、数据看板。用 Vue 3 加 Vite 是目前启动最快、生态最稳的组合。很多开发者在创建工程时纠结用 vue-cli 还是 Vite新版官方推荐 Vite启动速度比 Webpack 快一个量级。注意 Node.js 版本要 16 以上版本低了 Vite 直接报错。npm create vitelatest travel-admin -- --template vue cd travel-admin npm install npm install axios vue-router pinia element-plus npm run dev创建工程后要装的东西就四个axios 做 HTTP 请求vue-router 做路由pinia 做状态管理element-plus 做后台 UI 组件库。旅游后台的表单、表格、日期选择器用 element-plus 能省大量时间。npm run dev启动后访问 5173 端口看到 Vite 欢迎页就说明环境通了。装依赖时最大的坑是版本冲突。比如 Vue 3.4 和某版 element-plus 的 peer 依赖不匹配npm install会报 ERESOLVE 错误。我的处理方式是把 node_modules 删掉重装或者用npm install --legacy-peer-deps跳过依赖检查。另外 vue 路由的createWebHistory在部署到 nginx 时需要配置 try_files 回退到 index.html不配的话刷新二级路由页面会 404。3.2 与后端联调axios 封装、路由守卫和 token 刷新后台管理页面的核心逻辑是登录后拿 token带着 token 调后端接口。axios 封装得好不好直接决定联调效率。我会在src/utils/request.js里创建一个实例baseURL 指向网关地址请求拦截器统一加 token响应拦截器统一处理后端返回的状态码。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:8080/api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(travel_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(travel_token) router.push(/login) return Promise.reject(new Error(登录已过期)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(travel_token) router.push(/login) } else { ElMessage.error(error.message || 请求失败) } return Promise.reject(error) } ) export default request这段封装的逻辑是请求发出去之前从 localStorage 取 token 加到请求头后端返回 401 时清除本地登录态并跳回登录页其他错误弹出提示。旅游后台的订单列表、产品管理都需要登录态token 过期后如果不做统一处理每个页面都要写一遍判断逻辑太容易漏。路由守卫配合这段封装在router.beforeEach里检查登录态未登录直接重定向到/login。3.3 旅游项目里的 M3U8 播放与地图引入两个高频页面旅游后台里有两类页面非常高频景区视频预览和地图选点。景区宣传视频通常以 M3U8 格式存在 CDN 上Vue 里播放 m3u8 不能直接用原生 video 标签需要引入 hls.js。用户搜索里「vue播放m3u8免安装」指的就是这个场景hls.js 通过 MSE 在浏览器播放 HLS 流不需要装任何插件。import Hls from hls.js export function playM3u8(videoEl, url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoEl) hls.on(Hls.Events.MANIFEST_PARSED, () { videoEl.play().catch(() {}) }) } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src url videoEl.play().catch(() {}) } }这里判断两个分支普通浏览器用 hls.js 播放iOS Safari 原生支持 m3u8 直接赋值。调用时把 video 元素的 ref 和 m3u8 地址传进去就行。地图选点页面用 MapBox 比较多引入mapbox-gl后设置 accessToken 和中心点坐标拖拽地图时监听moveend事件把经纬度回填到表单里。这两个组件的共同点是都不需要写大量模板代码封装成公共组件后各页面复用。4. UniApp 一套代码出 App 和小程序工程结构与双端适配4.1 新建 UniApp 工程时的 TypeScript 与 manifest 初始配置UniApp 是这套项目里连接 App 和微信小程序的桥梁。用 HBuilderX 创建工程或者用 CLI 方式创建都行CLI 方式适合团队协作。用户搜索里「uniapp 创建项目支持ts」说的是建工程时勾选 TypeScript 模板我在正式项目里会选 ts 模板类型提示能避免不少低级错误。工程创建后目录里有pages、static、manifest.json、pages.json几个核心部分。pages.json控制页面路由和导航栏样式manifest.json控制应用配置。manifest 里要配的东西很多开发者工具里的小程序 AppID、App 端的应用名称和图标、H5 端的路由模式都在这里。我一般先把微信小程序 AppID 配上App 的图标和启动图留到打包前再处理因为这两个资源文件会被各平台商店审核临时换会拖慢上线节奏。{ name: 旅游通, appid: , description: 旅游项目多端客户端, versionName: 1.0.0, versionCode: 100, transformPx: false, mp-weixin: { appid: wx1234567890abcdef, setting: { urlCheck: false, es6: true, minified: true }, usingComponents: true }, app-plus: { usingComponents: true, nvueStyleCompiler: uni-app, compilerVersion: 3 } }versionName 和 versionCode 这两个字段特别容易忽略。App 上架安卓应用市场时每次更新 versionCode 必须递增否则应用市场拒绝覆盖安装。transformPx设为 false 比较好px 单位在真机上就是逻辑像素不会因为设备宽度变化导致布局错乱。mp-weixin里的 setting 配置控制小程序构建时的行为urlCheck 开发阶段关闭上线前要打开。4.2 微信小程序打包与 App 上架发行流程和版本号管理UniApp 开发完成后要分两条线走微信小程序走「发行到微信小程序」App 走「原生 App 云打包」。小程序端在 HBuilderX 里点发行菜单生成一个dist/build/mp-weixin目录用微信开发者工具导入这个目录就能预览和上传。关键点在于不能用微信开发者工具直接打开项目根目录那样会把源码的结构错误地当成小程序目录。App 上架安卓应用市场是另一套流程。云打包时选择「使用云端证书」或者用自有证书打包产物是 apk。上架到华为、小米、应用宝这些市场时每个市场要求的权限声明不太一样。旅游 App 通常需要定位权限开发者工具里要在 manifest 的 app-plus 权限配置里声明location相关权限否则真机调用定位接口会直接失败。版本号管理我的习惯是versionName 用语义化版本比如 1.2.0versionCode 用纯数字从 100 开始递增。每次发版前先在 manifest 里改好这两个值再打包。如果先打包后改版本号应用市场的覆盖安装会提示签名或版本问题。小程序端的版本号不在这控制是在微信公众平台上传代码时填的两边的版本号独立管理。4.3 多端差异与定位、截屏、后台运行的 API 兼容UniApp 最理想的体验是一套代码三端跑通但实际开发中总有 API 不兼容的地方。定位功能是最典型的例子。微信小程序里获取地理位置必须通过wx.getLocation并且要在 manifest 里声明requiredPrivateInfosApp 端用的是 HTML5 Plus 的定位 API。UniApp 提供了uni.getLocation封装但返回的坐标系在不同端上不一样小程序返回的是国测局坐标App 在高德地图模式下返回的是高德坐标传给后端时统一转成 WGS84 才不出乱子。「uniapp 后台运行监测定位 userlocationbackground」这个需求在 App 端要申请后台定位权限Android 上还得在 manifest 里加前台服务类型声明。这块每个手机厂商的权限设置又不一样小米和华为的后台定位权限路径不同测试时要专门找几台主流机器跑一遍。获取版本号的需求也是多端不一致。H5 端可以用uni.getSystemInfoSync().appVersion但小程序端这个字段不准确。我一般在小程序端用uni.getAccountInfoSync().miniProgram.envVersion判断是开发版还是体验版App 端用 plus.runtime.version 拿原生版本号。这些分端判断写在#ifdef条件编译里UniApp 编译时会自动裁剪掉非目标平台的代码。// #ifdef APP-PLUS const version plus.runtime.version console.log(App 版本号:, version) // #endif // #ifdef MP-WEIXIN const accountInfo uni.getAccountInfoSync() const envVersion accountInfo.miniProgram.envVersion console.log(小程序版本模式:, envVersion) // #endif条件编译是 UniApp 多端适配里最好用的工具被#ifdef包裹的代码只会在对应平台被编译。App 上用 plus 对象、小程序上用 wx 对象都能在各自平台安全调用不会因为对象不存在而报错。5. 避坑旅游项目三端开发里最常见的 5 个翻车现场5.1 现象小程序里调用uni.setStorageSync存对象取出来变成字符串原因是对 UniApp 存储机制理解不到位。小程序端会把非字符串类型序列化处理但有些平台版本只存字符串对象会被JSON.stringify变掉类型。解决存取数据时显式做 JSON 序列化和反序列化不要依赖框架自动处理。// 存 uni.setStorageSync(travel_user, JSON.stringify(userInfo)) // 取 const userInfo JSON.parse(uni.getStorageSync(travel_user) || {})这个坑在天气接口返回的缓存数据、用户上次浏览的线路列表里最容易出现。如果取出来发现是字符串而不是对象多半就是序列化处理不当。5.2 现象MyBatis 二级缓存开了以后订单列表刷新后数据不对原因是二级缓存跨 SqlSession 共享旅游订单表频繁更新缓存没有及时失效。MyBatis 二级缓存适合低频更新的配置表不适合订单这种高频写表。解决订单相关 Mapper 关闭二级缓存只用一级缓存或者用 Redis 做显式缓存并设置过期时间。mapper namespacecom.travel.order.mapper.OrderMapper cache evictionLRU flushInterval60000 size512 readOnlytrue/ /mapper这个配置只适合产品分类、景区介绍这类几乎不变化的表。订单表加上这个配置后用户支付成功但列表还是旧状态非常影响体验。线上业务把二级缓存关掉配合 Redis 的Cacheable注解按 key 过期更可控。5.3 现象uniapp 项目里console.log在微信小程序控制台不打印原因是小程序的日志输出默认被过滤了。微信开发者工具的 Console 面板默认只显示 Info 级别以上的日志console.log在开发版里要打开「显示 Info 级别日志」的开关。还有 App 端的日志要连上 vConsole 才能在真机调试里看到。解决调试时先在开发者工具里确认过滤条件再查代码里是不是误用了console.debug。这个问题排查起来特别耗费时间我见过有人以为是代码没执行实际上日志一直有输出只是被工具过滤了。真机调试时直接看 vConsole 的 Log 面板不要看服务端的日志。5.4 现象Spring Boot 服务启动时报ClassNotFoundException: javax.xml.bind.JAXBException原因是 JDK 版本升级后移除了 Java EE 模块。Spring Boot 2.7 搭配 JDK 17 时MyBatis 的某些依赖需要引入 jaxb 依赖。解决方式有两种把 JDK 退到 1.8 和 Spring Boot 2.7 配合或者保留 JDK 17 并显式引入 jaxb-api 依赖。dependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency旅游项目里用 Java 8 还是 17 不取决于个人偏好取决于团队已有的公共组件。如果公司内部框架依赖 Java 8 的语法强行上 17 会让构建过程出现大量兼容问题。5.5 现象Vue 项目路由切换后页面空白nginx 部署的二级路径刷新 404原因是createWebHistory模式在 nginx 没有配置 fallback。解决nginx 的 location 块加try_files $uri $uri/ /index.html;。如果是部署在子路径下Vue 的base配置和 nginx 的location前缀必须保持一致。location /admin/ { alias /var/www/travel-admin/; try_files $uri $uri/ /admin/index.html; }旅游后台部署在域名子路径时最容易遇到这个问题登录页能打开点击路由跳转后刷新就 404。开发环境不会暴露这个 bug因为 Vite 的开发服务器内部做了 history fallback只有部署到 nginx 才现原形。6. 跑通这套项目的验证清单按这个顺序检查少走三周弯路拿到项目源码后有固定的验证顺序按这个顺序排查比乱翻代码高效得多。第一步验证后端基础设施确认 Nacos 启动在 8848 端口用curl http://127.0.0.1:8848/nacos/v1/console/health/liveness看返回结果再把每个微服务的启动日志里搜Registering service关键字确认注册成功。第二步验证数据库连接每个服务的 dataSource 配置单独检查MyBatis 的mapper-locations路径写错会在启动时报Invalid bound statement。第三步验证 Web 端本地跑npm run dev后浏览器打开登录页先看 Network 面板里登录接口是否走通403 多半是网关白名单没配。第四步验证小程序端微信开发者工具导入dist/build/mp-weixin后预览重点看请求有没有被拦截开发工具里 URL 校验默认打开本地后端接口会被拦要在 mp-weixin 的 setting 里关掉 urlCheck。第五步验证 App 端云打包后装到测试机先看定位权限弹窗有没有出现再看版本号能不能正常获取。游项目最容易出问题的不是单个端点而是端与端之间的信息同步。产品在 Web 后台上下架小程序端要实时生效这个链路里如果走的是拉取模式客户端要加下拉刷新如果走推送模式要检查 WebSocket 连接。我的习惯是上线前在测试环境完完整整走一遍「后台建产品 → 小程序下单 → 后台审核 → App 查看订单」的主链路这条链路通畅项目就能提交验收。这行做久了你会发现多端项目最大的成本不是写代码是联调时来回对齐接口和排查环境差异。希望这套梳理过的流程能帮你在下一回接手旅游类多端项目时少踩几个坑。本文还有配套的精品资源点击获取