
读这个标题的人大概率不是第一次做管理系统了。SpringBoot Vue这套组合在市面上已经被写到烂随便一搜就是xx管理系统源码但真正能落到生产环境、经得起业务推敲的智慧旅游项目反而不多。很多所谓的源码就是把增删改查套了一层景点外壳页面华丽但业务逻辑经不起问。我这次拆解的这座智慧旅游服务平台不堆砌花哨功能重点放在“游客、景区、商家、平台”四方角色如何在一个系统里跑通以及前后端在真实联调、部署过程中最容易卡住的那些细节。项目源码和配套文档的结构我会一并说明白方便你直接拿去做毕业设计、课程设计或者二次开发的底子。1. 项目形态与核心需求拆解先把这个项目到底是什么说清楚。智慧旅游服务平台本质上是一个面向C端游客、B端景区/商家、以及后台运营方的多角色业务系统。市面上很多同类项目只做了游客端的小程序/H5和后台管理忽略了商家入驻、订单分账、景区导览这些真实业务流转环节导致项目看起来完整但一讨论业务就露馅。这个项目的核心业务流可以拆成四条主线游客侧注册登录、浏览景点与攻略、在线购票、预订酒店/民宿、查看旅游线路、发表游记评价、投诉建议。商家侧景区/酒店/旅行社入驻申请、商品上下架、订单处理、结算对账。平台运营侧内容审核、数据统计、用户管理、 banner 配置、系统参数维护。公共支撑地图导览、天气接入、路线推荐、消息通知、支付回调。我见过不少项目把四条线强行压成两张表最后做出来就是“景点信息表 订单表”支付对接没有、审核流程没有、商家端就是摆设。这个项目在设计时做了比较务实的取舍游客端、商家端、平台后台三端分离角色权限走 RBAC核心业务表按领域拆分。从技术角度看这项目不是简单的 CRUD 堆积。它涉及了文件上传、富文本内容管理、地图坐标存储与展示、订单状态机流转、支付回调验签、定时任务处理过期订单、Redis 缓存热点景点数据等多类后端常见场景而这些恰好是企业里问得最多的点。注意真正的智慧旅游平台重点不在“智慧”二字的技术包装而在数据能否流转起来。游客行为数据、商家经营数据、平台运营数据如果互相割裂那和传统 OA 没有本质区别。2. 技术选型理由与整体架构设计2.1 为什么是 SpringBoot Vue而不是微服务项目定位是中大型单体应用面向的是毕设、课设、小规模商用场景没有自建机房和千亿级并发的顾虑SpringBoot Vue这是当下最稳妥的选择。SpringBoot 的自动装配机制极大简化了 Spring 体系的配置成本内嵌 Tomcat 让部署变成“一个 jar 包扔到服务器”这对没有专职运维的团队来说是刚需。Vue 这边选择的是 Vue 2 Element UI不是 Vue 3 Element Plus。原因很现实网上可参考的组件案例、踩坑博客、二次封装方案Vue 2 的存量远超 Vue 3如果你学校或者公司的基础设施里还跑着老项目Vue 2 的经验可以无缝迁移。另外项目代码生成器、权限指令、动态路由这些工具链在 Vue 2 下非常成熟。你说 Vue 3 组合式 API 写起来更爽我承认但在这个项目里稳定性和可参考性优先。2.2 前后端分离架构与目录规划前后端分离是标配但分离到什么程度有讲究。这个项目的分离不只是“前端一个项目、后端一个项目”这种物理隔离更体现在开发流程和部署方式上后端只提供 RESTful API不做页面跳转所有接口统一返回ResultT结构体。前端通过 Nginx 反向代理/api前缀解决跨域和接口转发。静态资源图片、景点介绍附件、上传文件单独走文件存储路径不混入前后端工程目录。后端目录我按 DDD 的轻量思想做了分层但没有过度设计src/main/java/com/example/tourism/ ├── common/ # 通用返回体、异常处理、常量、工具类 ├── config/ # 全局配置跨域、拦截器、Redis、MyBatis-Plus ├── controller/ # 接口层只负责参数接收和结果封装 ├── service/ # 业务层事务和核心逻辑都在这一层 ├── mapper/ # MyBatis-Plus 数据访问层 ├── entity/ # 数据库实体映射 ├── dto/ # 前端交互的数据传输对象避免实体直接暴露 └── vo/ # 视图对象按前端需要组装前端目录则按业务模块划分常见的views/按角色拆成admin、merchant、client三个子目录公共组件放在components/下API 请求统一放在api/模块下每一个页面组件对应一个 API 文件避免请求代码散落在页面里。2.3 核心依赖清单及其版本坑依赖版本是这类项目最容易翻车的地方直接贴出我实测可用的版本组合组件版本说明JDK1.8不要用 JDK 17 跑 SpringBoot 2.x会有一堆 module 访问报错SpringBoot2.7.x2.x 最后的稳定大版本社区资料最多MyBatis-Plus3.5.x注意确认和 SpringBoot 2.x 的兼容性MySQL5.7 或 8.08.0 需要配置时区建议直接 5.7 省心Redis5.x用默认配置跑热点缓存即可Vue2.6.x配合 Element UI 2.15.xNode14.x/16.xNode 18 打包老项目容易报 OpenSSL 错误建议用 16特别提醒如果你用了 SpringBoot 2.7 MyBatis-Plus 3.5.3 的组合application.yml里一定要写global-config: banner: false否则启动时会有无伤大雅但看着很烦的警告。另外SpringBoot 版本太高的坑主要在 javax 到 jakarta 的命名空间迁移老教程里的import javax.servlet在 SpringBoot 3.x 下全部编译不通过这也是我坚持让你用 2.7.x 的原因。3. 后端核心模块的实现思路与关键表设计3.1 用户体系与 RBAC 权限控制用户体系是平台的基石这个项目把用户分为四类角色普通游客、景区管理员、商家、平台管理员。如果你做过 RBAC 就知道最忌讳的做法是给每类角色建一张用户表字段大量冗余不说后续做统一登录、统一认证会痛苦到怀疑人生。这里的做法是一张sys_user表存账号密码和基本信息一张sys_role表维护角色一张sys_user_role关联表维护用户角色关系菜单权限通过sys_menu和sys_role_menu控制。游客、商家、管理员本质上只是同一张用户表上挂了不同角色。SpringBoot 集成 Spring Security JWT 做认证授权。流程是这样的用户登录时校验账号密码成功之后生成 token 返回前端前端每次请求在 header 里带上Authorization: Bearer token后端通过 OncePerRequestFilter 拦截请求解析 token 得到用户 ID并从 Redis 里取出该用户的权限标识列表。使用 Redis 缓存权限数据的好处是用户权限变更比如商家被平台下架不需要强制用户重新登录只要 Redis 里的权限数据刷新即可这比每次都查数据库高效得多。3.2 景点、线路、攻略的内容域表设计内容域的几张表是整个平台的数据底座需要仔细设计。首先是景点表scenic_spot除了基础的名称、简介、封面图、经纬度、开放时间、联系电话之外我特意加了一个status字段0 待审核、1 已上架、2 已下架所有景点信息必须先经过平台审核才能展示这是合规要求。旅游线路表travel_route和景点之间是多对多关系路由点按顺序排列所以加了一个route_order字段而不是简单地存一个景点 ID 列表。攻略表travel_guide的正文是富文本存储时用 HTML 格式预览时直接渲染其中封面图、标题、作者 ID、浏览量、点赞数、审核状态都是高频查询字段需要建好索引。三张表的关系你可以这样理解景点是基础素材线路是景点的有序组合攻略是基于景点和线路的 UGC 内容。在写列表查询时如果只查单表数据MyBatis-Plus 的Page分页足够用一旦要关联用户昵称、景点名称就要手写 XML 里的自定义 SQL用 LEFT JOIN 聚合并映射到 VO 上。这是这个项目里最考验基本功的地方。3.3 订单状态机与支付回调的流程设计订单是业务系统的核心状态字段不是简简单单一个status(0/1/2)就完事。这个项目里订单设计了七种状态状态值含义触发动作0待支付用户提交订单1已支付/待使用支付回调成功2已使用/已消费景区扫码核销3已完成订单核销后自动完成4已取消用户取消或超时未支付5退款中用户发起退款申请6已退款平台审核后原路退回状态流转的代码实现不能散落在 service 的各个方法里否则后续维护的人看着看着就疯了。这个项目用一个OrderStatusHandler来统一承载状态流转的判断逻辑核心是维护一张状态流转表只有合法的迁移路径才允许执行。比如待支付状态下可以执行“支付”和“取消”但已支付状态下不能执行“支付”。支付这块接的是微信支付 Native 支付。流程不复杂但细节多前端调用后端下单接口后端生成预付单并返回code_url前端用二维码展示支付成功后微信服务器异步回调后端接口回调里要验证签名、校验订单金额、防止重复通知然后更新订单状态。这里最容易出问题的点有两个回调地址必须是公网可访问的 URL且响应微信服务器必须返回{code: SUCCESS}的明文否则微信会一直重试回调。超时未支付订单的关单我用的是延迟任务方案用户下单时把订单号丢到一个 Redis 有序集合里score 设为超时时间戳定时任务每分钟扫一次把超时的订单捞出来执行关单。这个方案比简单的Scheduled轮询数据库高效一些实现也不复杂。4. 前端几个关键页面的落地细节4.1 前端权限控制与路由守卫前端权限控制的核心是“按钮级别”和“路由级别”双管齐下。路由级别用动态路由用户登录后后端返回该用户的菜单权限集合前端router.addRoutes动态挂载对应路由这样未登录用户即使手动输入 URL也因为没有对应路由而跳转到 404 页。按钮级别控制用的是自定义指令v-permission指令内部判断当前用户是否拥有指定权限标识如果没有就直接移除 DOM 元素。// 在 main.js 里注册全局指令 Vue.directive(permission, { inserted(el, binding) { const required binding.value const hasPermission store.getters.permissions.includes(required) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } })el-button v-permissionorder:refund typewarning退款/el-button这里有个实际坑要提醒动态路由在用户刷新页面后会丢失因为刷新时 Vuex 数据重新初始化动态 addRoutes 的路由又没了。解决办法是在main.js的全局前置守卫里加一个“是否已动态添加”的判断用户刷新后先调一次获取用户信息的接口拿到权限后再重新 addRoutes用 if 判断避免死循环。4.2 地图选点与景点坐标展示景点都有经纬度坐标后台地图选点这个交互做不好就特别低级。这里用的是 Vue 集成高德地图 JS API 2.0 的方案。做法是在index.html里引入高德地图的 JavaScript 文件script srchttps://webapi.amap.com/maps?v2.0key你的KeypluginAMap.Scale,AMap.ToolBar/script然后在组件里定义全局AMap变量地图初始化逻辑放在mounted生命周期里。地图选点功能的核心是给地图绑定click事件用户点击地图后通过e.lnglat.getLng()和e.lnglat.getLat()拿到坐标再回填到表单的隐藏字段里。展示端则是在景点详情页初始化地图使用AMap.Marker把坐标点标记出来。如果你不会申请高德 Key也可以用 Leaflet OpenStreetMap 的替代方案但对国内景点来说高德的 POI 数据更准地图瓦片在国内的加载速度也更好。4.3 H5 视频播放 M3U8 流媒体的兼容处理热门景区会做实时直播或者风景慢直播这类视频流大多是 M3U8 格式HLS 协议。原生video标签不支持播放 M3U8需要在项目中集成hls.js这个库。npm install hls.js --save前端播放逻辑封装成一个通用组件template video refvideoRef controls/video /template script import Hls from hls.js export default { name: HlsVideo, props: { src: { type: String, required: true } }, mounted() { this.initVideo() }, methods: { initVideo() { const video this.$refs.videoRef if (Hls.isSupported()) { const hls new Hls() hls.loadSource(this.src) hls.attachMedia(video) this.hls hls } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS直接赋值 src video.src this.src } else { this.$message.error(当前浏览器不支持视频播放) } } }, beforeDestroy() { this.hls this.hls.destroy() } } /script特别提醒M3U8 播放时如果视频地址是 HTTPS、页面是 HTTP会出现混合内容拦截视频一片黑。开发环境下确保后端视频服务地址和前端协议一致生产环境统一走 HTTPS。4.4 富文本编辑器与图片上传联动攻略编辑这块用的是vue-quill-editor或者wangeditor。选择后者的话注意v4 版本已经不再维护直接装 v5 的wangeditor/editor-for-vue。工具栏里图片上传功能默认是把图片转 Base64 塞进内容里这样做有两个隐患一是 HTML 体积爆炸二是数据库存储压力大。必须改成自定义上传逻辑用户选择图片后先调用后端上传接口拿回图片 URL再插入编辑器内容区。import { Boot } from wangeditor/editor import { uploadImage } from /api/file const editorConfig { MENU_CONF: { uploadImage: { async customUpload(file, insertFn) { const res await uploadImage(file) insertFn(res.data.url, res.data.url, res.data.url) } } } }后端文件上传接口的存储路径要根据日期分目录避免一个目录下文件过多文件名使用 UUID 重新命名防止中文文件名乱码和路径遍历攻击。5. 数据缓存、定时任务与部署环节的实战坑点5.1 热点数据缓存策略与一致性处理景点列表、首页 banner、旅游攻略 Top10 这些数据属于读多写少的高频数据适合放 Redis 缓存。这里的实现思路是查询时先查缓存缓存没有则查数据库并回填缓存设置过期时间比如 30 分钟景点信息或攻略被后台修改后主动删除对应缓存而不是更新缓存这样能避免并发下缓存和数据库不一致的问题。缓存穿透的防护要做否则有人恶意请求不存在的景点 ID请求会直接打到 MySQL 上。简单方案是布隆过滤器更简单粗暴的方案是查询结果为空时也缓存一个空值设置较短的过期时间比如 5 分钟这样能在绝大多数场景下挡住非法请求。5.2 定时任务处理订单超时与数据统计SpringBoot 的Scheduled在这个项目里承担两件事一是每分钟扫描 Redis 有序集合中过期的待支付订单触发关单操作二是每天凌晨 2 点定时统计前一天的运营数据写入报表表。这里有个值得注意的坑Scheduled默认是单线程串行执行的如果你配置了两个任务其中一个执行时间长另一个会被阻塞等待。解决方式是在启动类上加EnableScheduling并配置一个线程池支持多任务并行执行Configuration public class SchedulingConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }5.3 前后端打包部署与 Nginx 配置后端打包成 jar 包用 Maven 的package命令执行生成文件在target/目录下。前端打包执行npm run build产物在dist/目录。生产环境用 Nginx 托管前端静态文件并反向代理后端 APIserver { listen 80; server_name your-domain.com; # 前端静态资源 root /usr/share/nginx/html; index index.html; # 解决 Vue Router history 模式刷新 404 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意proxy_pass http://127.0.0.1:8080/;结尾的/绝对不能漏。proxy_pass http://127.0.0.1:8080;会把完整的/api/login路径传给后端而加了/之后会去掉/api前缀再转发后端 Controller 里定义的 RequestMapping 就不会出现前缀不匹配的问题。前端打包之后布局异常的排查也是重灾区。Vue 项目打包后容易出现样式丢失、图片 404、图标不显示常见原因主要是这三个一是publicPath没配置打包后的 CSS/JS 路径是绝对路径/js/app.js部署到子目录就直接挂掉需要在vue.config.js里配置publicPath: ./二是路由模式用了 history刷新后 Nginx 没配置try_files飞到了 404 页面三是图片、字体等静态资源用了绝对路径引用解决办法是统一走相对路径或者使用 CDN。5.4 跨域、数据库连接与端口冲突排查实录开发环境下前端跑在localhost:8081后端跑在localhost:8080必然有跨域问题。后端我这里没有用 Spring 的CrossOrigin注解一个个加而是在WebMvcConfigurer里写了一个全局跨域配置允许的路径是/api/**允许的请求头包含Authorization允许的方法包含 OPTIONS、GET、POST、PUT、DELETE。这样保持了配置统一不会出现“这个接口能访问、那个接口不能被访问”的玄学问题。数据库连接建议使用 HikariCPSpringBoot 2.x 默认就是它。连接池配置不要照抄网上的大数值小项目给maximum-pool-size: 10minimum-idle: 5connection-timeout: 30000足够了。如果 MySQL 8.0 连不上十有八九是时区问题JDBC URL 后面手动加上serverTimezoneAsia/Shanghai和useSSLfalse。端口冲突的问题用一句命令排查netstat -ano | findstr 8080 # Linux/macOS 用 lsof -i:8080定位到占用进程后要么杀掉进程要么改 SpringBoot 配置里的server.port。如果改端口要注意前端vue.config.js里的 devServer proxy 目标地址也得同步修改这是新手最容易漏掉的一处。6. 源码结构说明与配套文档的写作方案6.1 源码目录结构与关键代码位置对照一套能直接拿去用的源码目录结构必须清晰关键代码位置可快速定位。这个项目的后端关键代码位置对照如下功能模块关键文件/包路径登录认证与 JWTcontroller/AuthController.java、utils/JwtUtil.java权限过滤拦截config/SecurityConfig.java、filter/JwtAuthenticationTokenFilter.java景点管理controller/ScenicSpotController.java、service/impl/ScenicSpotServiceImpl.java订单流转controller/OrderController.java、service/impl/OrderServiceImpl.java、handler/OrderStatusHandler.java支付回调controller/PayNotifyController.java、service/impl/WxPayServiceImpl.java文件上传controller/FileController.java、config/FileStorageConfig.java数据统计controller/StatisticsController.java、service/impl/StatisticsServiceImpl.java前端的关键代码位置对照如下功能模块关键文件动态路由与权限src/router/index.js、src/store/modules/permission.js全局请求封装src/utils/request.js后台管理布局src/layout/index.vue地图选点src/views/admin/scenic/ScenicEdit.vueM3U8 播放组件src/components/HlsVideo/index.vue实体类、Mapper、Service 的代码生成我用的 MyBatis-Plus 代码生成器一键生成之后手动补业务逻辑能省下大量重复工作。生成器模板里注释要留好把字段注释和表注释带出来后续看代码不用反复开数据库看表结构。6.2 配套文档的层次划分与写作建议很多同学项目做完了才想起来写文档最后写出来的东西就是操作手册和代码注释的缝合怪答辩的时候老师一问三不知。我建议这套项目的文档分为四个层次各有侧重项目部署文档面向想让项目跑起来的人。包含环境要求、初始化数据库脚本的执行步骤、后端application.yml的各项配置说明、前端npm install到npm run serve的完整过程、默认账号密码列表。这一部分一定要写清楚很多人在环境依赖上卡死。系统设计文档面向需要理解项目的人。重点写系统架构图、功能模块清单、数据库 ER 图、核心表结构说明、关键业务流程登录认证、下单支付、审核上架的流转描述。每个功能模块配一张页面截图和核心逻辑描述。接口文档面向需要二次开发的开发者。推荐直接用knife4j或者springdoc-openapi自动生成 OpenAPI 文档不用手写。但要注意在注释里写清楚参数的取值范围、是否必填、示例值这样生成的接口文档才有实用价值。答辩/汇报 PPT 大纲面向课程答辩或毕业答辩。按照“项目背景 → 需求分析 → 技术架构 → 功能演示 → 核心难点与解决方案 → 总结与展望”这条线组织内容。这里特别强调一个答辩技巧不要通篇讲你做了什么功能要挑一两个你解决得最漂亮的技术难点展开讲比如订单状态机、支付回调幂等性设计这些才是加分项。6.3 项目 README 文件的写法和开源注意点如果这套源码要开源或者作为作品集给面试官看README 是门面。一份合格的 README 至少要包含项目简介、技术栈列表、功能截图、快速开始步骤、目录结构说明、默认账号、常见问题列表。我见过很多项目源码好端端的README 就一句话“旅游平台系统”面试官下载下来跑不起来第一印象直接崩掉。另外要注意如果你在网上参考了其他项目的代码要留意许可证问题。GPL 协议的代码是不能直接抄进自己项目再闭源的。个人学习和毕设用途问题不大但如果要商用或公开开源需要谨慎处理。7. 从零复现这个项目的实施路线建议7.1 四个阶段的推进节奏与验证标准如果你打算从零开始做这样一个项目我建议按四个阶段推进每个阶段有明确的验收标准避免一盘散沙做到一半想放弃。第一阶段跑通最小闭环。目标是完成“用户注册登录 → 浏览景点列表 → 下单 → 模拟支付成功 → 查看订单”这五步。这一阶段不需要做商家端和管理后台但数据库表设计要一步到位后端接口返回的数据结构也要按最终格式设计。第二阶段完善角色与权限。给用户表加角色字段引入 Spring Security JWT实现基于角色的权限控制。接着做商家入驻申请、平台审核、景点上架/下架这几条审核链路。做完之后验收标准是游客账号访问不到商家后台商家账号看不到平台管理菜单。第三阶段业务闭环深化。接入真正的文件上传、富文本编辑、地图组件把攻略发布、线路规划、视频直播这些内容型功能补上。同时把订单状态机、支付回调、退款申请这些涉及资金安全的逻辑做完整。第四阶段打磨与部署。写单元测试或者接口自测脚本把异常分支重复提交订单、重复支付回调、并发下单超卖都测一遍。然后配置 Nginx、部署到云服务器申请 HTTPS 证书把整个系统跑在公网上。这一步做完项目才算真正从“能运行”变成“能交付”。7.2 开发过程中的版本管理与备份策略开发过程中版本管理一定要用 Git不要觉得自己一个人写就不需要。我见过太多人写着写着把代码改坏了CtrlZ 都救不回来最后只能从头再来。推荐的 Git 提交频率是功能完成一个提交一个提交信息写清楚干了什么比如feat: 完成游客下单接口、fix: 修复支付回调重复通知导致订单状态错乱。数据库变更也要纳入版本管理。每次修改表结构导出一个新的 SQL 增量脚本放在sql/migrations目录下按日期命名。这样不管在哪台机器上部署执行一遍初始化脚本加增量脚本数据库环境就能保持一致。7.3 二次开发时可以扩展的方向这套项目源码的扩展空间很大按投入产出比排序我推荐这几个方向接入 Redis 分布式锁解决并发下单超卖问题这是面试高频考点。引入 Elasticsearch 做景点和攻略的全文检索替换掉现在的 MySQL LIKE 模糊查询。将小程序端纳入体系复用现有的后端接口不过需要处理微信登录态和接口鉴权。增加消息通知模块用 WebSocket 实现订单状态变更的实时推送。引入 Docker Compose 一键部署把后端、前端、MySQL、Redis 都容器化交付体验直线上升。我自己的体会是这类全栈项目最能锻炼人的地方往往不是某个技术栈有多深而是“把所有零件组装在一起还能稳定运转”的那种全局掌控感。你今天遇到的跨域问题、版本冲突、打包资源丢失都是未来工作里每天都要打交道的东西。做项目就像搭积木前期地基打得正后面每一层都顺前期图快跳过细节后面返工的成本只会翻倍。这套智慧旅游平台源码里我最满意的地方就是订单状态机那段设计——它不复杂但把业务边界画得明明白白这种思维方式比任何框架都值钱。