
简介面向旅游项目全栈开发场景的完整工程后端基于SpringCloudAlibaba、SpringBoot、MyBatis构建前端包含网页端、移动端和微信小程序端界面采用Vue与uni-app开发适合毕业设计、课程设计、工程实训或初期项目立项复现参考。压缩包共两千零一十二个文件含一千八百六十九个文档、五十八个XML配置、五十七个Java源码、六个数据库脚本及说明文档约118MB其中文档可作项目笔记与答辩材料参考数据库脚本便于初始化旅游业务数据。目前已有四十二人浏览学习源码与工程文件经测试运行、功能可用设计报告可借鉴答辩评审平均分达九十六分。可按说明文件复现项目也能基于现有模块扩展新功能整体结构清晰涵盖数据库、接口与页面适合学习交流与二次开发。1. 旅游项目多端工程先看清楚这套技术栈解决什么问题接到一个旅游类项目需求往往不只是「做个网站」而是同一套业务逻辑要同时跑在Web端、App端和微信小程序上。标题里这组技术栈——Spring Cloud Alibaba Spring Boot MyBatis 做后端Vue 做管理端或 H5UniApp 做跨端——基本是目前中小团队做旅游平台最务实的一套组合不是最前沿但胜在组件全、资料多、招人容易。用这套方案你可以把景点、线路、酒店、订单、支付、分销这些旅游业务的核心模块拆成微服务用 Nacos 管注册和配置用 Sentinel 给高并发查询兜底用 Gateway 做统一入口再用 Vue 和 UniApp 分别覆盖 PC 管理端与移动端用户。不过拿到这类工程结构时新手容易把时间耗在「怎么把五个端跑起来」上真正的难点其实在后面Nacos 版本和 Spring Boot 版本怎么配对、MyBatis 多数据源和分页插件怎么共存、UniApp 打包 App 和微信小程序时各自有什么坑、Web端和移动端共用接口时鉴权怎么统一。这篇笔记就是把我在类似项目里趟过的路、调过的参数和踩过的坑按顺序讲清楚从工程结构拆解到联调避坑照着做能少走很多弯路。2. 后端微服务骨架Spring Cloud Alibaba 组件怎么选、怎么配2.1 五大组件的取舍不是每个服务都要全部挂上Spring Cloud Alibaba 常被提起的五大组件是 Nacos注册中心配置中心、Sentinel流量控制、Seata分布式事务、RocketMQ消息队列和 Gateway/OpenFeign网关与远程调用。在旅游项目里这五个组件不是都要上尤其是中小型项目一上来就把 Seata 和 RocketMQ 全挂上反而会让部署和运维成本翻倍。我一般会按业务阶段来定起步阶段只上 Nacos OpenFeign GatewaySentinel 看情况开等订单、支付、分销这类需要强一致或异步削峰的场景出现后再逐步引入 Seata 和 RocketMQ。组件选型要和业务场景对上。比如景区门票秒杀或节假日抢购Sentinel 的流控规则能直接挡掉突发流量但如果是简单的后台管理 常规订单流程Sentinel 的熔断降级可以先只配置在查询类接口上写操作暂时不挂。同理Seata 解决的是跨服务事务像「下单同时扣库存、生成订单、锁优惠券」这种场景才需要如果只是单库事务用了反而拖慢速度。2.2 Nacos 版本与 Spring Boot 的配对这一步配错后面全是坑Nacos 和 Spring Boot、Spring Cloud Alibaba 之间的版本兼容是新手最容易翻车的地方。不是装最新版就一定好官方给出的版本对应关系必须遵守否则会出现「服务注册不上」「配置拉不到」「启动后过几秒就掉线」这类玄学问题。一个稳定组合是Spring Boot 2.6.x Spring Cloud 2021.0.x Spring Cloud Alibaba 2021.0.5.0 Nacos Server 2.2.0。这组搭配在旅游项目这种查询多、写少的场景下足够稳定。pom.xml 里核心依赖这样写properties spring.boot.version2.6.13/spring.boot.version spring.cloud.version2021.0.5/spring.cloud.version spring.cloud.alibaba.version2021.0.5.0/spring.cloud.alibaba.version /properties dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring.cloud.alibaba.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring.cloud.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring.boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement版本统一声明在 dependencyManagement 里子模块只管引入具体依赖、不写版本号这样能避免多个微服务之间依赖版本不一致。Nacos 的客户端版本由 spring-cloud-alibaba-dependencies 传递管理不需要单独指定单独指定反而可能和组件内部定义的版本冲突。2.3 配置中心拆分旅游项目多环境怎么组织 Nacos 配置旅游项目通常有 dev、test、prod 三套环境加上多个微服务模块Nacos 的命名空间Namespace和配置分组Group需要一开始就规划好。我常用的策略是每个环境建一个命名空间用 Namespace ID 区分每个微服务在命名空间下建自己的 Data ID内容用 YAML 格式。例如订单服务的配置 Data ID 可以命名为order-service.yaml Group 用TOUR_GROUP。bootstrap.yml 这样配置spring: application: name: order-service cloud: nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos discovery: namespace: ${NACOS_NAMESPACE:dev} config: namespace: ${NACOS_NAMESPACE:dev} group: TOUR_GROUP file-extension: yaml shared-configs: ->SpringBootApplication MapperScan(com.tour.order.mapper) public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }application.yml 里对应配置mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.tour.order.entity configuration: map-underscore-to-camel-case: true call-setters-on-nulls: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case开启后数据库的create_time字段能自动映射到实体类的createTime。call-setters-on-nulls设为 true 是为了查询结果里 null 字段也能回填到对象避免前端拿到缺字段的 JSON。log-impl在开发环境打印 SQL线上环境记得关掉否则高并发下日志量非常吓人。SQL 里有个常见场景是订单查询需要按创建时间排序、按状态过滤XML 中写动态 SQLselect idselectOrderPage resultTypecom.tour.order.entity.Order SELECT id, order_no, user_id, scenic_id, status, total_amount, create_time FROM tour_order where if teststatus ! null and status ! AND status #{status} /if if testuserId ! null AND user_id #{userId} /if if teststartTime ! null AND create_time gt; #{startTime} /if /where ORDER BY create_time DESC /selectwhere标签会自动去掉第一个多余 AND不用手工拼条件。gt;是 XML 里大于等于号的转义写法直接写会解析报错。时间查询条件在旅游业务里非常常见——查某天某个景区的订单量、查某个时间段的热门线路都要用这种方式。3.2 多数据源与分页一个配置类搞定旅游项目经常要读多个库比如订单库和景区内容库分离。MyBatis 多数据源推荐用MapperScan分别指定不同包路径配合ConfigurationProperties绑定多个数据源。这里要特别注意多数据源时事务管理器也要分别配置否则Transactional不知道管的是哪个数据源。Configuration public class DataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.order) public DataSource orderDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.content) public DataSource contentDataSource() { return DataSourceBuilder.create().build(); } Bean public SqlSessionFactory orderSqlSessionFactory( Qualifier(orderDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/order/**/*.xml)); return factory.getObject(); } }分页插件 PageHelper 在多数据源场景下要按数据源分别配置。一个容易忽略的坑是PageHelper 的dialect参数要明确指定数据库类型否则插件自动检测可能选错方言。MySQL 写dialect: mysql如果项目里还接了 PostgreSQL千万别共用同一套配置。3.3 MyBatis 缓存本地缓存生效与失效的边界MyBatis 的一级缓存是 SqlSession 级别的默认开启二级缓存是 Mapper 级别的默认关闭。在 Spring Boot 集成场景下SqlSession 的生命周期由框架管理一级缓存的意义不大开了反而可能查不到最新数据。二级缓存如果要做只能在查询频率极高、数据几乎不变的配置类表上开比如景区基础信息表。在 XML 里开启二级缓存cache evictionLRU flushInterval60000 size512 readOnlytrue/flushInterval60000表示每 60 秒自动清空一次缓存readOnlytrue表示缓存返回只读对象性能更高但如果你在代码里修改了查询结果对象会直接串改缓存里的数据。所以遇到「改了缓存对象导致后续查询数据异常」这种问题多半就是 readOnly 设错了。旅游项目里景区介绍、线路详情这种内容型数据适合开二级缓存订单、支付这类数据不要去碰。4. Vue 与 UniApp 双端页面同一套业务逻辑怎么拆4.1 Vue 管理端后台系统和 PC 官网的分工旅游项目里 Vue 一般承担两个角色一是运营后台管理景点、线路、订单、分销商二是 PC 官网前台旅游线路展示、下单。这两个角色最好拆成两个 Vue 工程不要揉在一个项目里因为后台要的是表格、表单、权限控制官网要的是 SEO、路由预取、首屏性能。拆开后用同一个后端接口层通过NODE_ENV区分接口地址// src/config/index.js const envConfig { development: { baseURL: http://localhost:8080, portalBaseURL: http://localhost:8081 }, production: { baseURL: https://api.tour.com, portalBaseURL: https://portal.tour.com } } export default envConfig[process.env.NODE_ENV]路由守卫里做登录态校验未登录用户访问下单页时跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(tour_token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })本地存储的 token 在余额充足的情况下可以换用 HttpOnly Cookie 方案能挡掉大部分 XSS 窃取。但 HttpOnly Cookie 对跨域请求的withCredentials配置要求更高前后端都要配合。开发环境下可以用 Vue DevTools 直接调试接口返回和路由状态比看 console 日志高效得多。4.2 UniApp 工程结构pages 分包与公共组件抽取UniApp 一端三端的特性大家应该都清楚但真正上手后代码怎么组织才是重点。把业务页面按模块分包主包只放 TabBar 页面和公共组件景区详情、线路列表、订单结算这些页面全部放分包能显著降低微信小程序的启动体积。manifest.json 里配置分包{ mp-weixin: { appid: your-wx-appid, setting: { urlCheck: false }, optimization: { subpackages: true } } }urlCheck: false只在开发时用让你能把请求打到本地后端。上线前务必改回 true否则真机预览时所有 request 都会被微信拦掉。UniApp 里请求封装要同时考虑 Web、App、小程序三端差异比如 header 里携带 token 的方式、上传文件时的chooseImageAPI 在三端的差异。我习惯用uni.request包一层 Promiseexport function request(options) { return new Promise((resolve, reject) { uni.request({ url: https://api.tour.com${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message, icon: none }) reject(res.data) } }, fail: (err) reject(err) }) }) }三端行为差异主要出现在这些地方App 端跨域限制少但需要配置合法域名微信小程序要求所有请求域名必须在小程序后台配置且必须是 HTTPSH5 端受浏览器跨域限制需要后端开启 CORS。所以网关的跨域配置要连这几种来源都考虑到下面联调章节我会细说。4.3 H5 端指向两个域名开发和生产的接口切换热词里提到的「uniapp 封装 h5 如何指向 2 个域名」这在旅游项目里很典型H5 页面要同时对接官网 API 和微信网页授权 API。UniApp 里的解决方案是通过process.env.NODE_ENV区分环境再用一个config模块管理两个域名// utils/config.js const API_CONFIG { development: { mainDomain: http://localhost:8081/api, authDomain: http://localhost:8080/auth }, production: { mainDomain: https://api.tour.com/api, authDomain: https://auth.tour.com } } export function getMainBaseURL() { return API_CONFIG[process.env.NODE_ENV].mainDomain } export function getAuthBaseURL() { return API_CONFIG[process.env.NODE_ENV].authDomain }在微信浏览器里打开 H5 时getAuthBaseURL负责拼微信网页授权跳转链接getMainBaseURL负责业务接口调用。还有一层容易忽略的问题微信内 H5 分享给好友时分享出去的链接应该带一个channelwx之类的参数这样后端能识别流量来源也能在前端判断是否需要走微信授权流程。H5 在 iOS Safari 和微信内置浏览器里localStorage的持久化机制不完全一样有些版本在无痕模式下读写会抛异常建议封装存储层时用 try-catch 包裹。5. 网关鉴权与联调避坑三端并发调试时最容易翻车的点5.1 Gateway 统一鉴权Fitter 怎么写才不重复登录三端共用同一套后端接口鉴权不能在每个微服务里各做各的否则用户在一个端登录了另一个端还要重新登录。标准方案是在 Gateway 层写全局过滤器解析 token 并校验把用户信息放到请求头里转发给下游服务。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Autowired private StringRedisTemplate redisTemplate; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 白名单登录、注册、景区列表等无需鉴权 if (path.startsWith(/auth/login) || path.startsWith(/scenic/list)) { return chain.filter(exchange); } String token request.getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { return unauthorized(exchange); } String userInfo redisTemplate.opsForValue().get(token: token.substring(7)); if (userInfo null) { return unauthorized(exchange); } ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Info, userInfo) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } Override public int getOrder() { return -100; } }token 存在 Redis 里过期时间由 Redis 的 TTL 控制。用户改密码或被踢下线时直接删掉 Redis 里的 key就能实现「立即失效」。注意Ordered返回-100让鉴权过滤器顺序靠前执行。白名单用前缀匹配在旅游项目里够了但如果后续做更精细的权限控制建议把白名单挪到 Nacos 配置里不要硬编码在过滤器里。还有一点坑Gateway 里用ServerWebExchange修改请求头必须在chain.filter之前调用mutate()完成否则下游服务拿不到X-User-Info。5.2 跨域配置Web 端、H5 端、小程序端的 CORS 差异跨域配置是联调时的重灾区。Web 端管理后台和 H5 页面跑在浏览器里跨域是浏览器强制的App 端和小程序端不受 CORS 限制但小程序有域名白名单限制。Gateway 层统一配置 CORSConfiguration public class CorsConfig { Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); } }addAllowedOriginPattern(*)和setAllowCredentials(true)可以同时使用但如果你用的是addAllowedOrigin(*)和 credentials 一起用会被浏览器拒绝。前端如果发现「跨域报错但后端明明配置了」先确认是不是多个跨域配置覆盖了比如 Nginx 里也配了Access-Control-Allow-Origin会和 Gateway 的响应头叠加导致冲突。前后端联调时在浏览器 Network 面板里看Access-Control-Allow-Origin响应头是不是正确值能快速定位。5.3 避坑旅游项目三端联调常见问题排查根据我在类似项目里的踩坑经验三端联调中最常见的问题集中在以下几个地方现象一本地开发时UniApp 编译到微信开发者工具里所有请求都报url not in domain list。原因微信小程序开发时开启了域名校验而本地请求地址是http://localhost:8080不在白名单里。 解决在微信开发者工具右上角「详情」-「本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。上线前必须把这个选项关掉并在小程序后台配置正式域名。现象二App 端请求正常但同一套代码编译到 H5 端后接口全部 401。原因App 端uni.setStorageSync存 token 后H5 端 localStorage 里没有 token或者 H5 端跨域请求没带Authorization头。 解决查一下 H5 端登录成功后 token 是否写入了uni.getStorageSync(token)以及在 request 封装里 H5 端是否正确带了 header。关键在于 request 封装在 ap 端和小程序端取 token 的 API 相同但 H5 端如果页面刷新时存储被清空需要从 URL query 或 Cookie 里恢复 token。现象三网关过滤器放行了/scenic/list但下游服务还是返回 401。原因下游服务里可能自己配了拦截器或 Spring Security 配置只信任网关转发的请求检测不到Authorization头就拒绝。 解决检查下游服务的拦截器是否放行了未曾到达网关的内部路径或者把网关下发的X-User-Info头作为下游服务的鉴权依据不要在前后端之间同时传递两套 token。网关转发时把原始Authorization头去掉避免下游再走一遍 token 解析造成重复鉴权。现象四微信小程序里uni.request能通但uni.uploadFile上传图片报错。原因uploadFile 的 header 参数和 request 不一样默认不带上业务自定义 header后端拿不到 token 就拒绝了。 解决uni.uploadFile也要同样设置header: { Authorization: uni.getStorageSync(token) }文件上传接口建议单独做一个带 token 校验的端点不要和普通业务接口混在一起。6. 上线前最后一步从开发到生产环境的几个必备调整开发环境能跑通只算完成了一半真正上线前有几个调整必须做否则用户一多就开始出问题。首先是 Nacos 集群部署生产环境不要用单机 Nacos至少三节点构成集群配置和注册数据用 MySQL 持久化。Nacos 默认内置 Derby 存储生产环境换成 MySQL 后先初始化数据库脚本再启动集群节点之间才能同步数据。Nacos 的application.properties里开启 MySQL 存储spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://127.0.0.1:3306/nacos?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueuseUnicodetrueuseSSLfalseserverTimezoneAsia/Shanghai db.user.0nacos db.password.0nacos注意 Nacos 2.x 的客户端和服务端之间除了 HTTP 还有 gRPC 长连接端口是主端口 1000比如 8848 对应 9848。防火墙和安全组别只放开 8848否则服务注册和配置拉取会超时控制台却显示一切正常。MyBatis 的 SQL 日志在生产环境一定要关掉上面提到过log-impl会打印每条 SQL并发一大磁盘 IO 就扛不住。可以换一种方式只在出问题时用动态开关打开比如通过 Nacos 配置项控制log-impl的值排查完再关掉。Gateway 的日志级别也建议从 DEBUG 调整到 INFO避免每个请求的转发细节都刷屏。图片和上传文件不要存在本地磁盘旅游项目的景区图片、用户头像、评论图片都是高频读取资源。用 MinIO 做对象存储配合 Nginx 反向代理对外提供 HTTPS 访问。MinIO 接入 Spring Boot 时我只在配置里指定 endpoint、accessKey 和 secretKey上传时调putObject返回的 MinIO 文件路径存数据库访问时拼接 Nginx 的对外地址。最后一步是上线前的压测和验证。用 JMeter 做一次最简单的冒烟级验证登录获取 token、查景区列表、提交一个订单三个接口各跑 100 个线程看平均响应时间和错误率。如果 100 并发下查询接口超过 500ms先看是不是 SQL 没走索引再看 Sentinel 熔断规则有没有挡掉慢调用。三端真机验证时微信小程序用体验版App 用 TestFlight 或安卓测试包H5 直接扫域名看——核心链路走通三端一致这个项目才算真正可以交付。希望这些调整能帮你在上线前少折腾几个晚上。本文还有配套的精品资源点击获取