
古城景区管理系统这种项目做过的都知道看着模块不多真要把业务闭环理清楚也得花不少功夫。这套基于SpringBoot Vue的前后端分离系统涵盖游客端在线购票、景区信息展示、公告发布、订单管理、后台数据统计这些核心功能源码、数据库脚本和设计文档都齐全算是这类管理系统里结构比较完整的一套。不管是拿来交课程设计、毕业设计还是给中小型景区做信息化管理做参考都挺有借鉴价值。这篇文章不打算从头到尾罗列代码而是把项目从数据库建模到前后端联调再到部署踩坑的完整链路拆开讲一遍。我会把当时设计表结构的思路、购票扣库存为什么必须加事务、前端路由守卫怎么拦截未登录用户、打包部署时遇到的几个经典问题全部摊在明面上说清楚。如果你正准备动手写一个类似的管理系统这篇文章应该能帮你绕开不少弯路。1. 项目定位与整体技术选型思路1.1 古城景区管理的核心业务范围先明确这套系统到底在解决什么问题。古城类景区的日常管理表面上看着简单实际跑一遍业务流程就会发现环节不少游客要在线查看景区介绍、选择游玩日期、下单买票景区运营方要维护景区基本信息、发布公告通知、核销门票管理人员还得能实时掌握每天卖了多少票、收了多少钱、哪个时间段是客流高峰。我把这些需求归拢了一下大致可以划分成两条业务主线游客端前台景区列表与详情、在线选票下单、查看个人订单、提交评价。管理端后台景区信息维护、门票库存与价格管理、订单查询与统计、公告发布、用户管理。系统本身不追求大而全核心就是把这套“游客在线买票、管理员后台管票”的闭环走通。边界划清楚之后后面做数据库设计和接口设计就顺畅很多。其实很多项目做到一半开始改需求、加表加字段根本原因就是前期没有把业务边界和角色权限想明白导致代码越写越乱。1.2 技术栈选择的逻辑与取舍技术选型这部分我用的是SpringBoot Vue MySQL这套经典组合而且SpringBoot版本选的是2.x。为什么这么选说几个实际考虑大多数教学环境和现有模板都基于SpringBoot 2.x网上资料多遇到问题容易搜到解决方案。SpringBoot 3.x虽然新但要求JDK 17起步很多学校机房和老服务器不一定能跑起来。Vue 2 Element UI或Vue 3 Element Plus这类后台管理组件库非常成熟表格、表单、弹窗、分页整套UI组件拿来即用不用自己造轮子。MySQL 5.7或8.0作为关系型数据库对于景区这种数据量级完全够用事务支持和统计查询都很稳定。还有一个容易被忽略的点为什么不用微服务因为景区管理系统本质上属于中小型单体应用用户量级和业务复杂度都远没到需要拆分的程度。如果一上来就上微服务、消息队列、分布式缓存反而会把简单项目搞复杂。做技术选型的核心逻辑是“够用就好”把余力放在业务实现和代码质量上。2. 数据库建模与关键表结构设计2.1 业务表拆解与关系梳理数据库设计是整个系统的基础表结构如果设计得不合理后面写接口时就会到处补字段、拼SQL。我当时梳理出来的核心表一共七张每张表都对应一条清晰的业务链路。表名主要作用关键关联用户表(users)存游客注册信息关联订单表管理员表(admin)后台登录账号独立于前台用户景区表(scenic_area)景区基本信息与门票库存关联订单明细订单表(orders)一次购票生成的订单主记录关联用户、订单明细订单明细表(order_item)订单里的每张票关联景区、订单公告表(notice)管理员发布的景区公告独立表评价表(comment)游客购买后的评价内容关联订单、景区订单表和订单明细表为什么要拆成两张因为一个订单可能包含多张票比如游客一次性买了两张成人票、一张儿童票。如果只做一张订单表每张票就要存一条订单记录订单状态、下单时间、总金额这些信息全都要重复存一遍不但冗余统计“单量”的时候也会算错。拆成主表和子表之后订单表存一次下单信息订单明细表存三种票的明细逻辑就顺了。景区库存字段我直接设计在景区表里字段叫stock表示每日可售门票总数。这种设计在中小景区场景下是够用的。如果景区分淡旺季、不同日期库存不同就要单独拆一张库存表按日期维度去控制那是更复杂的场景这里不做扩展。2.2 字段类型、索引与订单金额的细节处理字段设计看着琐碎其实里面藏着很多坑。我重点说三个地方。第一金额字段必须用DECIMAL而不是float或double。浮点数在计算时会有精度丢失问题比如19.99加上0.01用浮点算出来可能变成20.00000000004。我最初做的时候图省事用了double结果月底对账时差了五分钱排查了半天才发现是精度问题。金额统一用DECIMAL(10,2)最稳妥。第二订单号不要用自增ID。自增ID暴露在客户端容易被人猜到订单量而且多表联查时容易混淆。我用的策略是时间戳加随机数订单号 年月日时分秒 四位数随机数单机场景下基本不会重复。如果要更严格可以加一个流水号表统一生成。第三常用查询字段必须建索引。这张表结构设计里最容易被忽略的就是索引。景区列表查询按景区名称模糊搜索、订单查询按用户ID和时间范围筛选这些字段如果不加索引数据量一旦上千条就会明显变慢。我当时给users表的id、orders表的user_id和order_no、order_item表的order_id和scenic_id都加了索引查询性能完全不用担心。给一段核心表结构的示意实际字段可以按需调整CREATE TABLE scenic_area ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景区名称, description TEXT COMMENT 景区介绍, price DECIMAL(10,2) NOT NULL COMMENT 门票单价, stock INT NOT NULL DEFAULT 0 COMMENT 可售票数, image_url VARCHAR(255) COMMENT 展示图片, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );status字段用TINYINT存枚举值而不是直接用字符串这样数据库占空间小、查询效率高Java端用常量或枚举类去映射可读性也不差。3. 后端SpringBoot核心功能实现详解3.1 工程骨架与接口层设计规范后端工程我是按标准的分层结构来搭的controller、service、mapper、entity、config、common这几个包各司其职。Controller只负责接收参数和返回结果Service层写业务逻辑Mapper层操作数据库。很多新手容易犯的错误是把业务逻辑全部堆在Controller里一个接口方法写几百行后面想复用或排查问题都很痛苦。接口设计上用了RESTful风格比如GET /api/scenic/list获取景区列表GET /api/scenic/{id}获取景区详情POST /api/order/create创建订单GET /api/order/myOrders查看我的订单POST /api/admin/login管理员登录所有接口统一返回一个Result对象包含code、message、data三个字段。这样前端在axios响应拦截器里只要判断code就能知道请求成不成功不用每个接口单独处理异常。全局异常处理用RestControllerAdviceService层抛出业务异常时统一转换成对应的错误码返回给前端。MyBatis-Plus这个工具非常推荐使用它对单表CRUD提供了现成的BaseMapper像景区列表的分页查询、用户信息的增删改查基本不需要手写SQL。只有订单统计这类多表聚合查询才需要写自定义SQL。3.2 购票扣库存与登录鉴权的关键逻辑购票下单是整个系统的核心业务流程这里有一条必须守住的红线扣库存和生成订单必须放在同一个事务里否则就会出现超卖或者数据不一致。我当时是这么写的Transactional public OrderResult createOrder(OrderRequest req) { // 1. 校验景区是否存在且上架 ScenicArea scenic scenicMapper.selectById(req.getScenicId()); if (scenic null || scenic.getStatus() ! 1) { throw new BizException(景区不存在或已下架); } // 2. 校验库存是否充足 if (scenic.getStock() req.getTicketCount()) { throw new BizException(库存不足); } // 3. 扣减库存 scenic.setStock(scenic.getStock() - req.getTicketCount()); scenicMapper.updateById(scenic); // 4. 生成订单和订单明细 // 5. 返回订单号 }这里我只做了逻辑层面的防超卖用updateById直接覆盖库存字段在高并发下其实是存在线程安全问题的。如果门票抢购峰值很高更稳妥的是用乐观锁更新库存时加上stock 扣减数量的条件同时判断受影响行数。不过对于大部分景区管理系统并发量远没到这个级别逻辑校验加事务已经够用了。登录鉴权我用的JWT方案。用户登录成功后后端签发一个带过期时间的token返回给前端。前端每次请求在请求头带上token后端通过一个拦截器统一校验。校验token的拦截器要排除登录、注册、获取景区列表这些不需要鉴权的公开接口避免游客都进不来。有一点提醒大家注意JWT的密钥不要硬编码在代码里放到application.yml配置文件中管理。如果要改造权限系统可以从简单的角色字段开始给用户表加一个role字段拦截器里判断角色的接口权限即可不需要一上来就引入Spring Security那套东西学习成本不低对小型项目是过度设计。4. 前端Vue页面构建与联调实战4.1 工程结构与路由权限设计前端工程我用的Vue CLI脚手架目录结构按功能拆api目录统一放接口请求views目录放页面组件router目录配路由store目录做全局状态管理。组件不追求过度封装但api请求统一集中到一个文件里方便管理接口地址。路由设计上分了两个层级游客可访问的公开页面和管理员的后台页面。游客页面包括首页景区列表、景区详情、登录注册后台页面包括订单管理、景区管理、公告管理等。用路由守卫实现访问控制router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin) !token) { next(/login) } else { next() } })这段逻辑很简单但非常实用。前端守卫虽然拦不住刻意绕过的请求但能提供正常用户的操作体验——未登录点后台菜单时直接被弹回登录页而不是进入页面后才报接口错误。4.2 核心页面交互与接口联调经验页面实现这块最核心的是景区列表页、购票页和后台管理页。景区列表页用了Element UI的卡片组件展示景区图片和简介分页组件配合后端的分页接口。图片加载失败时要显示默认占位图这个不起眼的细节如果忽略列表页一旦有图片失效整排卡片会很难看。购票页的交互关键在于日期和数量选择。日期选择器禁用已售罄的日期数量选择时实时计算总金额并在提交前二次确认。这里我踩过一个坑前端计算金额后在提交时又把金额传给后端后端直接用前端传过来的金额入库。这样设计等于把金额控制权交给了前端非常危险。正确做法是后端根据景区表里的单价重新计算金额前端的金额只是展示用。后台管理页主要在表格加弹窗的组合。景区信息编辑、公告发布都用Dialog弹窗完成表格操作列放编辑和删除按钮。删除操作必须加一个二次确认的弹窗提示防止误点把数据删了。订单列表的筛选条件包括订单号、用户账号、下单时间范围后端用MyBatis-Plus的条件构造器拼动态查询条件。axios封装这块也值得说。我在拦截器里统一做了token注入和错误提示service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response response.data, error { if (error.response.status 401) { router.push(/login) } Message.error(error.response.data.message || 请求失败) return Promise.reject(error) } )统一在拦截器里处理401状态码后端token过期时前端会自动跳转登录页不用每个页面重复写一套判断逻辑。5. 部署上线与高频问题排查记录5.1 前后端环境搭建与打包部署项目跑通本地联调之后部署也是容易出问题的环节我把整个流程和踩坑点一起说清楚。后端打包部署比较标准用IDEA里Maven的package命令打成jar包然后在服务器上执行java -jar。打包前要注意application.yml里数据源的地址、用户名、密码开发环境和生产环境要分开配置。我在一个项目里就吃过亏把开发库的账号密码直接打进了生产包上线后连接的是测试库配置半天对不上。前端部署麻烦一点。Vue项目构建后生成的是静态文件需要放到Nginx的html目录下。Nginx还需要配置反向代理把/api开头的请求转发到后端服务端口。同时要配置try_files解决前端路由刷新404的问题location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这行配置很关键。如果不加try_filesVue的BrowserRouter模式在访问/scenic/detail这类路径时刷新页面就会返回404。原因在于前端路由是前端控制的Nginx默认找不到对应的物理文件就报404而try_files会把所有未匹配的路径都转回index.html由Vue路由去解析。前后端联调时的跨域问题我推荐优先用前端开发服务器的proxy代理解决。在vue.config.js里配置devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }开发环境这么配前端请求/api/xxx就会自动转发到后端地址浏览器看到的请求是同源的不会触发跨域。如果后端也同时配置了CORS两者可能会重复处理反而会导致部分请求出问题。我的经验是开发环境用前端proxy生产环境用Nginx反向代理后端接口保持简洁不在代码里加跨域处理。5.2 常见问题速查与排查思路项目开发过程中遇到的坑和解决思路整理成表格方便查阅。问题现象根本原因解决方法后端启动报数据库连接失败驱动类或账号配置错误检查application.yml数据源配置确认MySQL服务已启动日期字段显示比实际少8小时数据库时区问题数据库连接URL增加serverTimezoneAsia/Shanghai前端请求接口报跨域错误开发环境未配置代理在vue.config.js配置devServer.proxy打包部署后刷新404Nginx未配置try_files增加try_files $uri $uri/ /index.html接口报500但后端日志无异常可能被全局异常处理器吞掉在全局异常处理器中打印完整堆栈日志Maven依赖下载慢或失败默认中央仓库访问慢配置阿里云Maven镜像加速数据库时区这个问题值得多说一句。国内服务器时区默认是东八区但MySQL连接如果没指定时区JDBC驱动可能用系统默认时区去解析前后会差出8个小时。排查的时候看着订单时间明明是下午三点下单数据库里却存成了上午七点这种问题仅靠看代码很难发现一定要检查连接串参数。还有一个非常常见的坑是图片上传后访问不到。系统里景区图片如果只是存在本地磁盘路径部署后前端页面加载不出来。本地联调时后端http://localhost:8080/upload/xxx.jpg能访问是因为后端应用直接暴露了静态资源。生产环境部署时要么在后端配置静态资源映射要么用独立的图片服务器要么直接把图片转成Base64存库不推荐。最省事的方案是给SpringBoot配置虚拟路径映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); } }配置完这个图片的访问路径才真正稳定。最后说一个团队项目里容易“互相踩脚”的问题多人写代码时接口变动频繁前端调后端接口时发现字段对不上。我后来养成了一个习惯每个接口写完先在Swagger或Knife4j上过一遍确认返回字段和前端约定一致再进入联调。这个小动作能省掉大量“前端说缺字段、后端说没问题”的扯皮时间。这套古城景区管理系统做下来最花时间的不是写代码而是把业务边界和表结构梳理清楚。只要这两件事做扎实后面的编码、联调、部署都是水到渠成的事。如果你正在做类似系统可以从订单和库存表入手先把核心表建全再往细节里填整体节奏会顺很多。