
做了几个旅游类的管理系统以后我越来越觉得古城景区这类业务其实是“麻雀虽小、五脏俱全”。明明看着只有售票、导览、公告几个功能但真要落地门票订单、游客身份、导游分配、商户管理、数据统计全搅在一起一不小心就做成一个大泥球。最近刚完成的一套基于SpringBoot Vue的古城景区管理系统我把源码、数据库、文档都整理了今天直接把我踩过的坑和几个关键设计思路摊开聊给正在折腾同类系统的朋友一个参考。这套系统解决的核心问题很明确把景区线下的人工售票、纸质登记、公告张贴搬到线上让管理员能管景区信息、门票库存、订单流水、游客数据让游客能在线浏览景区介绍、购买门票、查看导览视频。技术栈就是主流的SpringBoot后端 Vue前端数据库用MySQL文件存储接Minio视频播放走M3U8流媒体。适合正在做课程设计、毕业设计或者是小景区想低成本自建管理后台的团队参考。1. 项目全景与系统设计思路1.1 为什么选SpringBoot Vue这套组合现在做管理系统这套组合几乎成了默认起点但很多人只是“大家都在用所以我也用”没有想过它到底好在哪。我自己选它有四个实际原因一是分工清晰。SpringBoot负责提供REST接口Vue负责页面渲染和交互前后端通过JSON通信后端只关心业务逻辑和数据前端只关心展示和操作两边可以并行开发不用像JSP那样页面里塞满Java代码改个样式都得重启服务。二是生态成熟。SpringBoot的starter机制让集成变得很省事连MySQL、Redis、JWT、Minio都有现成的依赖不需要自己造轮子。Vue这边组件化开发写一次页面模板可以在多个地方复用比如门票卡片、公告列表这种组件后台和前台都能用同一套。三是部署灵活。后端可以打包成jar直接跑前端打包成静态资源后既能独立部署在Nginx也能直接放进SpringBoot的static目录一起启动特别适合小项目不想搞复杂运维的情况。四是调试方便。前后端分离以后前端可以用Vite开发服务器热更新后端用DevTools热重启改完代码马上能看到效果排查问题的效率比传统单体高很多。1.2 系统模块划分与业务流程古城景区管理系统我拆成了两个端游客端和管理端。游客端不需要登录就能浏览景区信息买票的时候再注册登录管理端登录后根据角色进入不同管理页面。核心业务模块包括景区信息管理景区介绍、开放时间、联系电话、位置地图、宣传图片和视频。门票管理门票类型成人票、学生票、套票、票价、库存、上下架状态。订单管理游客下单、支付状态、入园核销、订单查询与统计。游客管理注册游客的基本信息、历史订单、消费记录。公告管理发布景区公告、活动通知控制置顶和状态。导游管理导游信息录入、带团记录、游客评价。系统管理用户账号、角色权限、操作日志。业务流程上最核心的是“购票-支付-核销”这条线。游客选好门票类型提交订单时后端创建待支付订单同时扣减库存避免超卖支付成功以后订单变成已支付游客会拿到一个二维码入园时管理员扫二维码核销订单状态变为已使用库存不再恢复如果超时未支付订单自动取消库存回补。1.3 技术选型的几个额外考量基础组合之外我还加了两样东西Minio存储和M3U8视频播放。为什么呢景区宣传图片、导览视频这类文件不能直接存数据库也不适合塞在应用服务器的磁盘里否则后期扩容迁移会非常痛苦。Minio是一个兼容S3协议的对象存储服务部署简单用来存图片和视频源文件很合适。视频播放这块景区经常会需要网页端播放高清导览视频传统的MP4文件在浏览器里加载慢拖拽也不流畅所以我把视频切片成M3U8格式前端用播放器来拉流体验会好很多。这里我要强调一点技术选型不是越新越好而是看团队维护成本和项目实际规模。小景区项目没有必要引入微服务、消息队列这些重型组件SpringBoot单应用加MySQL加Minio已经能撑住日请求量几万次的场景后续如果量大了再考虑加Redis缓存和负载均衡也不迟。2. 数据库设计与核心表结构2.1 数据库设计要避免的三个坑做景区管理系统我第一次设计数据库时犯了几个错现在回头看都是比较典型的反面教材。第一个坑是表结构冗余严重。比如票务订单表里直接存了门票名称和景区名称这些字段其实可以通过门票ID关联查询出来没必要重复存否则以后改名称还要同步去改订单表非常容易漏。第二个坑是缺少状态字段。订单没有状态就无法区分待支付、已支付、已取消、已使用后面做统计和核销逻辑会变得非常别扭只能在代码里猜。第三个坑是忽略时间字段。很多表少了创建时间和更新时间出了问题想排查数据变更都找不到依据。所以这次设计我严格遵循了三范式的基本要求同时有意保留少量冗余字段来减少高频查询的JOIN比如订单表里放一个快照性质的门票名称但以门票ID作为逻辑主键关联既能保证显示方便又不会破坏数据一致性。2.2 核心数据表清单我把整个系统的表拆成以下几个模块每个模块都有明确的业务归属模块表名用途说明景区scenic_area存储景区基本信息、介绍、开放时间、联系方式门票ticket_type门票类型、价格、库存、状态订单ticket_order订单主表记录游客、门票、数量、金额、状态订单order_item订单明细表一张订单可能包含多种门票时使用游客tourist_user游客账号、昵称、手机号、密码加密存储公告notice公告标题、内容、发布时间、状态导游tour_guide导游姓名、简介、证件号、状态带团记录guide_record导游与订单/游客的关联记录管理员sys_user后台账号、角色、状态角色sys_role角色名称、权限编码权限sys_permission菜单、按钮、接口权限标识游客端和管理端的用户我分成了两张表因为字段和安全性要求差别很大。游客表面向C端需要记录手机号、第三方登录标识等管理员表面向内部需要和角色权限关联。混在一张表里会带来很多不必要的判断逻辑。2.3 字段设计与索引优化经验这里拿出两个关键表细说一下设计思路。订单表我设置的字段包括id、order_sn订单编号、tourist_id、ticket_id、ticket_name快照、quantity、unit_price、total_price、status、pay_time、use_time、cancel_time、create_time、update_time。订单编号必须唯一我采用时间戳加随机数生成保证并发下不会碰撞。状态字段用tinyint存枚举值1待支付、2已支付、3已使用、4已取消、5已退款后面写状态机逻辑的时候数字比字符串更高效。游客表字段包括id、username、password、phone、avatar、real_name、id_card、gender、status、create_time、update_time。密码用BCrypt加密存储绝不允许明文入库。手机号作为登录账号在业务上要求唯一所以建了唯一索引。索引这块我的经验是不要盲建。订单表按tourist_id、status、create_time三个字段建了联合索引因为后台最常见的查询就是“某个游客的订单列表”和“按时间范围查订单”。其他表只在经常作为查询条件的字段上加索引比如公告的status和publish_time门票的status。字段冗余和索引策略都是服务于具体业务查询的不是越全越好。3. 后端SpringBoot核心功能实现3.1 项目初始化与分层结构我习惯用Spring Initializr来创建项目Java版本选8或11SpringBoot版本选2.7.x。2.7.x目前比较稳定用的人多遇到问题容易查到资料比直接上3.x更稳妥。依赖方面我引入了spring-boot-starter-web、mybatis-plus、mysql-connector-java、lombok、jwt、hutool这些。项目分层我按经典的三层结构来做controller层负责接收请求和参数校验service层负责业务逻辑mapper层用MyBatis-Plus操作数据库。另外单独建了config包放安全配置、跨域配置、Minio配置common包放统一返回结果、异常处理、工具类entity包放数据库实体dto包放接口传输对象。MyBatis-Plus比传统MyBatis省事很多单表CRUD不用写SQL内置了BaseMapper的通用方法分页查询用Page对象配合分页插件就搞定。复杂查询用Select注解或者MyBatis的XML文件两套方式混用没有冲突。3.2 JWT登录鉴权与权限控制后台接口不能让所有人都能调我用了JWT做无状态认证。登录成功后后端生成一个Token里面带上userId和roleCode设置过期时间前端保存Token之后每次请求在Header里带Authorization字段。SpringBoot这边用一个拦截器统一校验白名单接口直接放行其他接口必须解析出合法用户才能进入。管理员接口还需要校验角色权限比如只有“超级管理员”能操作系统管理模块普通管理员只能操作票务数据。实现上我写了JwtUtil工具类负责生成和解析TokenSecurityInterceptor实现HandlerInterceptor接口做前置校验注册到WebMvcConfigurer的拦截器链里排除登录接口、注册接口、景区列表等不需要登录的公开接口。一个小经验JWT的密钥千万不要硬编码在代码里我放在application.yml配置文件中用base64编码后读取部署的时候通过环境变量注入这样即使代码泄露也不会直接暴露密钥。另外JWT是无状态的一旦签发没办法立刻作废所以我一般把过期时间控制在2小时并支持前端在Token快过期时自动刷新。3.3 门票预订与订单状态机门票预订是系统里最容易出错的地方核心难点是库存扣减和订单状态流转。我用数据库事务保证一致性下单接口方法上标注Transactional里面先查询门票库存是否充足充足则扣减库存再创建订单记录最后返回待支付订单信息。这里最忌讳的是“先创建订单再扣库存”的顺序会造成订单有了但库存没扣或者并发时库存变成负数。正确的顺序应该是先扣库存再记订单如果后面创建订单失败事务回滚库存自动恢复。订单状态流我用一个状态机常量类管理状态编码状态名允许流转到1待支付2已支付、4已取消2已支付3已使用、5已退款3已使用无4已取消无5已退款无支付回调我做了模拟实现这是很多课程设计项目会省略的地方。我配置了支付回调接口前端发起支付后由支付回调触发订单状态变更而不是前端直接调接口改状态。这样更贴近真实业务也避免用户通过伪造请求绕过支付。订单超时取消我用的是SpringBoot自带的Scheduled定时任务每30秒扫描一次超过30分钟未支付且状态为1的订单批量改为已取消同时回补库存。这个方案在数据量不大的时候非常可靠不需要引入消息队列。3.4 文件上传集成Minio景区宣传图、视频源文件都交给了Minio。Minio的部署很简单一个docker-compose文件就起来我一般同时创建两个bucket一个是images一个是videos权限设置为公开读、私有写。SpringBoot集成Minio我用的是io.minio:minio依赖初始化MinioClient后封装一个FileStorageService提供upload和delete方法。上传时处理一下文件名我用UUID拼接原始扩展名防止重名覆盖。图片上传后返回可访问的URL视频上传后返回管理端的一个标识然后由专门的转码任务去切片处理。在配置上要特别注意的是Minio的endpoint地址。开发环境我直接用http://localhost:9000部署到服务器后要改成服务器的域名或IP。如果在前端直连Minio的地址还要注意跨域问题我在Minio的启动参数里配了CORS规则允许指定来源访问。3.5 统计报表与数据导出管理后台首页需要展示几组核心数据今日订单数、今日销售额、本月游客数、门票库存预警。这些统计如果每次都实时查订单表数据量大了以后性能会比较难看。我的做法是新建一张daily_statistics表每天凌晨用定时任务统计前一天的数据入库首页直接查询这张表速度快逻辑也清晰。数据导出这块我用了阿里开源的EasyExcel支持订单列表导出成xlsx文件。设计原理是组装好数据List用EasyExcel提供的ExcelWriter写入HttpServletResponse的输出流前端拿到文件流后触发下载。这个功能在实际景区管理中很实用财务或者运营需要做月度对账的时候直接导Excel。4. 前端Vue核心页面与接口对接4.1 前端工程化与路由设计前端我用Vue3 Vite Vue Router Pinia Element Plus这套组合。Vite比Webpack快得多启动开发服务器基本秒开调试体验提升明显。Element Plus提供了表格、表单、弹窗、菜单等现成组件后台管理页面的开发效率非常高。路由设计我分两个层次面向游客的页面放在外层的Layout中面向管理员的页面放在内层的AdminLayout中。通过路由的meta字段标记requiresAuth和role配合路由守卫实现页面级权限控制。游客访问需要登录的页面会被重定向到登录页管理员访问没有权限的页面会得到404或403提示。项目目录结构上我把API请求单独抽出来放在api文件夹下每个模块对应一个JS文件统一封装axios实例配置请求拦截器自动携带Token响应拦截器统一处理错误码比如401时跳转登录页。这样页面组件里不会到处散落axios调用改接口地址或者加全局逻辑时只需要改一个文件。4.2 景区导览与地图展示游客端最体现景区特色的是导览页面。我引入了一个开源的地图库Leaflet加载OpenStreetMap的瓦片地图再通过后端接口获取景区的景点坐标列表在图上标记点位点击点位后展示景点介绍图片和文字信息。这里有个细节景点坐标数据需要在数据库里存经纬度通常用decimal(10,6)类型避免在坐标系转换上出问题。前端的Leaflet使用的是Web墨卡托坐标后端存的WGS84经纬度可以直接对应不需要额外转换。游客在导览页还能看到根据当前定位推荐路线这个功能我没有接第三方定位SDK而是用了浏览器自带的Geolocation接口获取经纬度经过计算和缓存后实现基本推荐逻辑。这种方式免去了申请API Key的麻烦在大多数浏览器上都能用。4.3 视频播放M3U8的免安装方案这个功能是被问得最多的。景区导览视频如果直接放MP4高清视频动辄几百MB游客用手机流量打开根本吃不消。我的做法是后端用FFmpeg把视频转码切片成M3U8HLS协议前端用依赖库播放。前端实现我选择了hls.js这个库去处理M3U8流在视频标签上通过JavaScript将HLS流转成浏览器能播放的格式。基本逻辑是判断当前浏览器是否原生支持HLS如果支持就直接设置video.src如果不支持就用hls.js加载M3U8再把实时媒体数据绑定到video元素上。这个方案不需要在用户电脑上安装任何播放器插件浏览器打开网页就能直接看兼容性实测下来Chrome、Edge、Firefox、Safari都没问题。需要注意的时候M3U8视频跨域请求需要在后端配置响应头Access-Control-Allow-Origin否则浏览器会拦截视频流请求。4.4 后台管理页面的权限与组件复用后台管理端我设计了几个核心页面仪表盘、订单管理、游客管理、门票管理、景区信息管理、公告管理、系统管理。其中订单管理和游客管理用Element Plus的表格组件加自定义查询表单门票管理用卡片加编辑弹窗景区信息管理用表单加图片上传组件。权限控制上我通过路由meta里的roles字段来控制菜单显示和访问比如系统管理页面只对超级管理员角色可见。前端还根据登录用户的权限列表动态生成菜单避免了在代码里写死每个用户能看到什么菜单这样新增角色时不需要改前端代码。图片上传组件我封装了uploadImage内部调用后端接口展示上传进度并在成功后回显图片地址。这个组件在景区信息管理、公告编辑、用户头像等场景中通用写一次就够。5. 部署发布与常见问题排查5.1 后端打包与配置分离后端打包我使用的是Maven的package命令最终生成一个可执行的jar包。但配置文件我会通过SpringBoot的Profile机制做环境隔离。application.yml放公共配置application-dev.yml放开发环境配置application-prod.yml放生产环境配置。启动时通过-Dspring.profiles.activeprod来指定环境。数据库连接、Minio地址、JWT密钥这些变量在prod配置里使用环境变量占位例如${DB_PASSWORD}在服务器的启动脚本中通过export或者systemd的EnvironmentFile注入。这样配置不会泄露在代码仓库里维护环境时也只需要改环境变量。另外我将jar包部署方式写成了一个部署脚本包含停止旧进程、备份jar包、启动新进程、检查健康状态四步。用nohup后台方式运行并将日志重定向到log目录。5.2 前端构建产物与SpringBoot整合前端构建后生成dist目录包含静态资源文件。我提供一个二选一的部署方案方案一独立部署把dist目录上传到服务器用Nginx托管同时配置反向代理将/api路径代理到SpringBoot端口。这个方案适合已有Nginx的团队性能高、隔离性好。方案二嵌入SpringBoot将dist目录里的文件复制到后端项目的src/main/resources/static目录下重新打包后端访问同一个端口时由SpringBoot返回前端页面和静态资源。这个方案适合快速演示和个人开发部署简单一个jar包搞定。我建议正式环境用方案一开发和演示可以用方案二。如果用了方案二需要特别留意前端的history路由模式SpringBoot需要配置一个转发规则把非接口的路径都转发到index.html否则刷新页面会404。我在项目中做了WebMvcConfigurer配置用一个接口处理所有未匹配的前端路由请求。5.3 高频报错与解决办法这里整理几个我实际开发中遇到最多的问题直接给结论端口被占用启动SpringBoot报端口被占用使用命令netstat -ano | findstr 8080查看占用进程或者直接换一个端口。开发时我还会遇到前端Vite端口和后端端口冲突建议前后端端口分开比如后端8080前端5173。跨域请求失败前端请求后端报CORS错误我统一在后端配置了跨域过滤器允许本地开发地址访问并且设置了exposedHeaders让前端能读取到及自定义的响应头。数据库连接失败最常见的原因是MySQL版本与驱动不兼容或者url里少了useSSLfalse、serverTimezoneAsia/Shanghai参数。我的经验是url中加characterEncodingutf8mb4否则存中文可能乱码。JWT解析失败签名不对或者过期时间异常检查密钥是否一致检查系统时间是否准确确保前后端对Token的处理一致。Minio上传超时上传大视频时默认会超时我在MinioClient初始化时设置较大的连接超时和读取超时时间并且在后端接口上限制MultipartFile大小避免一次传输过大。5.4 性能优化与安全加固性能方面我在订单列表查询接口加了字段缓存使用SpringCache同时缓存门票类型和公告信息因为这两个数据变化不频繁读多写少缓存命中率非常高。还在MySQL慢查询日志中排查了部分SQL给高频查询字段补了索引。安全方面除了JWT鉴权我还做了几件事接口参数校验使用Spring Validation注解在Controller层做非空、长度、格式校验避免脏数据进入数据库。统一异常处理用RestControllerAdvice捕获异常返回统一格式的JSON不直接把系统异常堆栈暴露给前端。SQL注入防护MyBatis-Plus默认使用预编译参数用占位符避免拼接SQL。密码加密存储所有用户密码都使用BCrypt哈希存储不保存明文。操作日志通过AOP切面记录管理端的增删改操作比如谁修改了门票价格、谁审核了公告出现问题可以溯源。6. 实操心得与扩展方向6.1 踩坑记录第一坑是MyBatis-Plus分页插件版本兼容问题。我一开始使用的分页插件版本和MyBatis-Plus核心版本不一致导致分页查询返回的总条数不对排查了很久才发现是版本冲突。解决方法是把分页插件版本调整到和核心包一致或者直接统一使用同版本依赖。第二坑是前端Vue Router的History模式。开发环境一切正常发布到服务器一刷新页面就404最开始我以为是Nginx配置问题后来才发现是前端路由模式和后端静态资源处理都需要配合。Nginx场景需要配置try_filesSpringBoot场景需要路由转发两者二选一即可。第三坑是Minio公开访问权限。上传图片成功但浏览器访问返回403原因是bucket的访问策略没设置成公开读。在Minio控制台里修改bucket的Access Policy或者通过API设置否则图片URL无法直接访问。第四坑是M3U8视频的CORS。本地开发时视频能播放部署后视频加载不出来控制台报跨域错误。需要确认视频所在域名的响应头允许请求来源在后端接口层面对媒体文件请求追加CORS响应头。这些坑单看都不难但每个都会浪费一两个小时。我整理进文档里了后续接手项目的同事能少走弯路。6.2 可以扩展的玩法这套系统做完以后我发现它还能很自然地延伸出不少实用功能。如果你有时间建议按这个顺序扩展一是接入真实支付。目前我做的支付是模拟回调真正商用的话可以接入支付宝或微信支付的沙箱环境把支付回调换成真实接口订单状态机不用大改。二是增加电子导览语音。景区里每个景点扫码听讲解很常见可以在现有景点表里增加音频字段在游客端导览页面增加音频播放控件数据模型上只需要加一个音频URL即可。三是做数据分析看板。前端用ECharts展示订单趋势、客流热力图、票务收入占比后端统计数据已经有了前端画图很快。四是引入Redis缓存。如果把JWT会话、门票库存、热门公告都缓存到Redis系统的并发能力能提升一个档次。库存扣减可以用Redis的原子操作避免数据库行锁压力。这一块我建议在项目上线以后按需引入前期用数据库事务完全够用。我个人在实际开发中的体会是做这类管理系统技术本身不是瓶颈瓶颈往往在于对业务状态流转的理解和边界情况的处理。门票库存的并发控制、订单状态的合法流转、文件访问的跨域问题这些细节才是真正体现项目质量的地方。把这套东西理清楚哪怕是新上手SpringBoot和Vue的开发者照着源码撸一遍也能对整套技术栈有一个非常扎实的认知。