☰
Spring Boot+Vue古城景区管理系统:从数据库设计到前后端分离实战
2026/10/22 3:28:49 网站建设 项目流程

从需求立项到最后跑通整个系统,我前后花了小两个月。这里把整个“古城景区管理系统”从设计到落地的过程整理成文,既算是对项目的复盘,也希望能给准备做同类前后端分离项目的朋友一份可参考的实操手册。

先说清楚这个系统到底是干什么的:它本质上是一个面向古城类景区的综合管理平台,覆盖了游客端、景区运营端和系统管理端三种使用场景。游客可以在线上查看景点介绍、在线购票、提交评论反馈;景区运营人员可以管理景点信息、处理票务订单、维护公告内容;系统管理员则负责账号分配、权限控制和数据统计。技术栈选的是 Spring Boot + Vue 这套前后端分离方案,配合 MySQL 数据库和一套完整的权限认证机制,也就是标题里说的“源码 + 数据库 + 文档”三件套。

这套系统适用的读者范围其实很广。如果你是刚学完 Spring Boot 和 Vue 的学生,它可以作为毕业设计或者课程项目的完整参考;如果你是中小型景区或文旅类公司的开发者,它可以直接作为信息化改造的底子;哪怕你只是想了解一个全栈项目从数据库设计到前后端联调要经过哪些环节,这篇文档也能帮你省掉很多弯路。

1. 系统整体设计与需求拆解

1.1 古城景区管理的核心痛点

传统古城类景区的管理,跟普通城市公园或者现代主题乐园有很大差别。古城往往区域面积大、出入口多、景点分布散,而且通常还夹杂着居民生活区和商业街区,这就导致几个非常实际的问题。

第一,信息更新滞后。景点开放时间调整、临时管控通知、民俗活动安排,这些信息如果靠张贴公告或者口头通知,传递效率极低,游客到了现场才发现信息对不上,体验很差。

第二,票务管理混乱。古城类景区很多时候是多景点联票,游客可能只想看其中一两个核心景点,也可能想买全城通票,线下售票很难灵活处理这种组合场景。人工核销票据的方式也容易产生漏洞。

第三,游客反馈无闭环。游客对某个景点的意见、对导览服务的评价,往往只能通过投诉电话或者意见簿记录,没有系统化的收集和分析手段,管理者很难及时发现服务短板。

所以这套系统的产品定位就很清晰了:用一套数字化平台把“信息发布、票务销售、游客管理、反馈收集”串起来,让景区运营从被动应对变成主动管理。

1.2 技术选型:为什么是 Spring Boot + Vue

选技术栈这件事,我其实纠结过一段时间。当时也考虑过用若依这类快速开发框架直接改,后来还是决定用 Spring Boot + Vue 从零搭建,原因有三个。

一是因为“可控性”。Spring Boot 的自动配置和 Starter 机制让后端开发量大幅减少,但又不至于像低代码平台那样把核心逻辑埋死在框架底层。像 JWT 认证、拦截器、异常处理这些关键链路,自己能控制每一行代码,后续扩展功能或者排查问题都更舒服。

二是 Vue 的生态足够成熟。前端用 Vue 全家桶(Vue Router + Vuex + Axios + Element UI),组件化开发的方式非常适合景区管理系统这种“大量表单 + 大量列表 + 中后台管理界面”的场景。Element UI 现成的表格、表单、弹窗组件,能让我把精力集中在业务逻辑而不是重复造轮子。

三是这套组合的市场接受度很高。无论你后续是要接外包项目、进公司做开发,还是拿这个项目作为个人作品,Spring Boot + Vue 这套组合的认可度都远高于小众技术栈,遇到问题能搜到的解决方案也多。

提示:如果你是完全没接触过前后端分离开发的新手,建议在动手之前花一周时间走一遍 Vue 的基础语法、Axios 的请求封装、Spring Boot 的 RESTful API 开发这三个重点。别急着直接看系统代码,否则容易迷失在大量文件里。

1.3 系统功能地图

从我最终落地的版本来看,整个系统按角色分为三类用户,对应的功能模块如下:

角色核心功能操作方式
游客(前端用户)景点浏览、在线购票、公告查看、评论反馈Web端操作
景区运营人员景点管理、票务管理、订单处理、公告维护后台管理端
系统管理员用户管理、角色权限、数据统计、系统配置后台管理端

这里我特别说一下为什么要把前端用户和管理后台分开。古城的游客端要的是“轻、快、直观”,一个景点列表页配详情弹窗就够了;而管理后台要的是“功能全、数据密、操作稳”,表格要能分页排序筛选,表单要带校验。两者混在同一个界面里,体验两头不讨好。

2. 数据库设计与核心表结构

2.1 数据库设计的三条原则

这套系统总共设计了十来张数据表,设计过程中我坚持了三条原则,这也是我在做过几个类似项目之后总结出来的经验。

第一条,业务表尽量拆分,不要试图用一张大表装所有字段。比如用户信息和用户角色我分开存,景点信息和景点图片也分开存,这样后续加字段、加功能就不会互相踩脚。

第二条,公共字段必须统一。每张表都带上create_time、update_time这两个时间字段,逻辑删除用deleted字段标记,不做物理删除。这样做的好处是数据可追溯,而且误删了也能通过改标记找回。

第三条,外键约束要克制。虽然 MySQL 天然支持外键,但在实际项目中我几乎不用物理外键,而是通过逻辑关联(Java 代码层面校验)来维护表间关系。原因很直接——物理外键删除数据时特别容易被约束卡住,联表查询的性能也受影响。

2.2 核心表结构详解

我挑几张核心表展开说说设计思路。

用户表(sys_user)

这张表存的不只是普通游客,还包含后台管理员和景区运营人员。所以除了常规的username、password、phone字段之外,还必须有role_id字段关联角色表。密码存储我用的不是明文,而是 MD5 加盐的方式。虽然更安全的 BCrypt 加密方案也成熟,考虑到这个系统的实际规模和部署环境,MD5 加盐已经能满足需求,而且后续替换加密算法也不影响表结构。

景点信息表(scenic_spot)

古城里的景点信息有几个特点:描述文字长、图片数量多、分类层级杂。因此表里设计了name(景点名称)、description(详细介绍)、type(景点分类)、address(位置)、open_time(开放时间)、ticket_price(门票价格)、cover_image(封面图)这些字段。这里还有个容易被忽略的点——状态字段status,用来标记景点是在正常开放、临时关闭还是维护中,游客端只会展示正常开放状态的景点。

订单表(order_info)

订单表是整个系统里业务逻辑最复杂的部分,字段多了很多:order_no(订单编号)、user_id(下单用户)、scenic_id(景点ID)、ticket_type(票务类型)、quantity(数量)、total_price(总金额)、status(订单状态)、pay_time(支付时间)等等。订单编号我是用时间戳加随机数生成的,比如20240615103022后拼接四位随机数字,这样既保证了唯一性,又能在排查问题的时候直接看出下单时间。

2.3 表关系梳理

简单梳理一下核心表的关系:

  • 一个用户可以有多个订单,所以用户表和订单表是一对多
  • 一个景点可以有多条评论,所以景点表和评论表是一对多
  • 一个角色对应多个用户,所以角色表和用户表是一对多
  • 一个订单必然对应一个用户和一个景点,所以订单表通过user_id和scenic_id分别关联用户表和景点表

游客端的分页查询,核心是一条连表 SQL:

SELECT o.id, o.order_no, o.quantity, o.total_price, o.status, s.name AS scenic_name, s.ticket_price AS scenic_price FROM order_info o LEFT JOIN scenic_spot s ON o.scenic_id = s.id WHERE o.user_id = ? ORDER BY o.create_time DESC LIMIT ?, ?

这条 SQL 用了LEFT JOIN而不是INNER JOIN,是因为景点信息理论上不会被删除,但万一出现异常数据,左连接也能保证订单记录仍然查得到。

3. 后端 Spring Boot 核心实现

3.1 项目分层:Controller 薄、Service 厚、Mapper 干净

后端我采用的是经典的四层结构:controller(接收请求)、service(业务逻辑)、mapper(数据访问)、entity(实体映射)。但这里要强调的核心思想是:Controller 要薄,Service 要厚,Mapper 要干净。

什么意思?所谓 Controller 薄,就是 Controller 只做参数接收、调用 Service、返回统一结果这三件事,绝对不要在里面写业务判断。所谓 Service 厚,就是所有业务规则、数据校验、事务控制都在 Service 层完成。所谓 Mapper 干净,就是 Mapper 里只放与数据库交互的 SQL 和接口,不放任何非数据库逻辑。

一个典型的 Controller 方法长这样:

@RestController @RequestMapping("/api/scenic") public class ScenicController { @Autowired private ScenicService scenicService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize) { return Result.success(scenicService.pageQuery(page, pageSize)); } @GetMapping("/detail/{id}") public Result detail(@PathVariable("id") Integer id) { return Result.success(scenicService.getDetail(id)); } }

统一返回结果Result是我自己封装的泛型类,包含code、message、data三个字段。成功时code为 200,失败时根据业务场景返回 400 或 500,前端 Axios 拦截器会统一判断。这样做的好处是前端处理异常逻辑非常统一,不需要每个接口单独去判断返回结构。

3.2 核心业务流程:购票下单是怎么实现的

购票下单是整个系统里最关键的链路,牵扯到库存、金额、订单三种状态的一致性。我拿这个流程举例说明后端是怎么处理的。

用户点击“立即购买”后,前端会请求一个下单接口/api/order/create,参数包括scenic_id、ticket_type、quantity。后端在 Service 层依次做四件事:

第一步,校验景点是否存在且处于开放状态。这里有个细节,票价的读取必须以数据库为准,前端传入的任何费用参数都必须忽略,这样才能防住恶意篡改。

第二步,校验购票数量是否在合理范围内。我设置了一个规则:单个订单最多买 10 张票,防止有人用脚本刷单。

第三步,计算总金额。根据景点表里对应的ticket_price乘以quantity,如果有优惠券逻辑也在这里叠加处理。

第四步,生成订单并返回订单编号。

整个方法加了@Transactional注解,保证订单插入和景点库存扣减这两个操作要么全部成功,要么全部回滚。实际测试中我也验证过:人为在中间抛一个异常,库存确实会回到原来的值。

3.3 登录认证与权限控制

这个系统的认证方式用的是 JWT(JSON Web Token)。用户登录成功后,后端生成一个 token 返回给前端,前端把 token 存在 localStorage 里并在每次请求时放到请求头Authorization字段中,后端通过拦截器统一校验。

拦截器是重点。我写了一个AuthInterceptor实现 Spring 的HandlerInterceptor接口,在preHandle方法里完成三步操作:从请求头里拿 token、解析 token 验证合法性、把解析出的用户信息放到 ThreadLocal 里供后续业务使用。

权限控制我用的方式不是引入 Spring Security 全家桶,而是自己封装注解。核心是在需要鉴权的接口上标注@RequireRole("admin"),然后在拦截器里判断当前用户的角色是否匹配。这样做的理由是这个系统的角色类型很少,只有管理员和运营人员两种后台角色,用轻量级方案反而更直观,排查权限问题也只需要看注解和拦截器两处代码。

注意:JWT 一旦签发,在有效期内无法主动注销。如果你后续要支持“强制下线”功能,建议在用户表里加一个token_version字段,每次修改密码或者主动退出的时更新这个值,校验 token 时对比版本号,不匹配则视为无效。

4. 前端 Vue 核心实现

4.1 工程化环境与目录结构

前端我用 Vue CLI 创建了标准工程,依赖管理用 npm。项目启动命令是npm run serve,默认跑在 8080 端口,通过vue.config.js里的 devServer 代理配置解决跨域问题。

目录结构按功能而非按类型组织,这是我后来重构过的结果。原先我按components、views、router这类类型分目录,但组件一多就乱套了。改为按功能分目录之后,结构清晰了很多:

src/ ├── api/ # 所有接口请求封装 │ ├── scenic.js │ ├── order.js │ ├── user.js │ └── auth.js ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── Pagination.vue │ └── UploadImage.vue ├── router/ # 路由配置 ├── store/ # Vuex 状态管理 ├── utils/ # 工具函数(含 request.js) └── views/ # 页面组件 ├── front/ # 游客端页面 └── admin/ # 管理后台页面

4.2 关键页面与组件逻辑

游客端首页的景点列表页,是整个前端的门面,也是我花心思最多的地方。

页面结构是这样的:顶部导航栏显示系统名称和登录状态;中间主体区域是一张景点卡片列表,每张卡片显示景点封面、名称、类型标签、门票价格和一个“查看详情”按钮。卡片下方是分页组件。

核心逻辑放在 Vuex 里。页面created钩子触发dispatch('scenic/fetchList', pageInfo)拉取景点列表数据存到scenic/list状态里,页面模板通过this.$store.state.scenic.list渲染数据。用 Vuex 而不是直接在组件里发请求的好处是:如果多个页面都需要景点数据,比如首页和搜索结果页,可以共享同一份数据,不需要重复请求,而且组件间通信也方便。

管理后台的门票管理页,我用了 Element UI 的el-table组件搭配el-dialog实现新增和编辑功能。表单校验用的是 Element UI 自带的rules配置,比如景区名称是必填项、门票价格必须是大于 0 的数字,这些校验规则直接写在表单组件的数据里,提交时统一触发。

4.3 Axios 拦截器与接口联调

联调是整个开发过程中最考验耐心的一环,但也最容易出问题。我使用 Axios 实例配合请求拦截器和响应拦截器,统一处理了三个问题:携带 token、错误提示、登录失效跳转。

请求拦截器的关键代码:

service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, error => Promise.reject(error) )

响应拦截器里,我会统一判断res.data.code是否为 200。如果为 200 直接返回res.data.data,简化调用方的数据处理;如果不为 200,则根据不同的错误码给出对应提示。其中特别注意处理 401 状态码,一旦遇到意味着 token 失效,需要清除本地存储的 token 和用户信息并跳转回登录页。

前端开发过程中我发现一个很重要的经验:接口联调阶段一定要让后端先跑起来,并且保证接口文档和实际返回结构一致。我在前期就吃过亏——后端返回的字段名是scenicName,前端按照scenic_name去取数据,结果整个列表渲染不出来。后来我主动跟后端约定所有接口返回都用驼峰命名,才把这类问题彻底消除。

5. 系统部署与运行实测

5.1 本地运行环境准备

运行这套系统需要的软件清单如下:

软件版本要求说明
JDK1.8及以上后端运行环境
Maven3.6以上后端依赖管理
Node.js14以上前端编译环境
MySQL5.7或8.0数据库
Navicat可选数据库可视化工具

数据库初始化这一步,我直接使用了项目里附带的init.sql脚本文件。在 Navicat 中新建一个名为gucheng_management的数据库,然后右键运行 SQL 文件即可,脚本会自动创建所有数据表和初始数据。初始数据里包含了一个管理员账号、几个测试景点和几条模拟订单,方便你启动之后立刻看效果。

5.2 运行步骤与配置要点

后端启动分三步:

第一步,修改配置文件application.yml。因为每个开发环境的 MySQL 账号密码都不一样,需要把url、username、password三个参数改成自己本地的。如果把端口改掉的话,记得同时修改前端里的代理配置,两者必须对应。

第二步,用 Maven 打包。操作很简单,在项目根目录执行mvn clean package,成功后会在target目录生成一个gucheng-0.0.1-SNAPSHOT.jar文件。

第三步,启动服务。可以在 IDEA 里直接运行主启动类,也可以到target目录下执行:

java -jar gucheng-0.0.1-SNAPSHOT.jar

前端启动更简单,在vue目录下执行:

npm install npm run serve

这里提醒一个高频问题:如果npm install踩坑,一般原因有两个。一是网络问题导致依赖包下载超时,解决方案是切换国内镜像源;二是 Node 版本跟项目的依赖不兼容,建议用 Node.js 14 或 16 的稳定版本跑,新版本有概率报错。

5.3 运行效果实测

后端启动成功后,控制台会出现Tomcat started on port(s): 8080的日志。前端打开浏览器访问http://localhost:8081,就能看到游客端的首页。

我实测了几个核心流程的完整表现:

  • 游客注册登录:访问首页点右上角“注册”,填写手机号、密码、验证码后提交,系统自动登录并跳回首页,全程约 3 秒。
  • 景点详情查看:点进景点卡片后,详情页会展示图片轮播、开放时间、门票说明等信息,接口响应在 100 到 200 毫秒。
  • 在线购票模拟支付:选择景点、填写数量、确认订单并点击支付,由于是演示环境没有对接真实支付通道,点击后直接模拟支付成功,订单状态自动从待支付变成已支付。
  • 管理后台操作:使用管理员账号登录后进入后台,可以新增景点、编辑订单状态、发布公告,所有表格都能分页查询和按条件筛选。

6. 常见问题与排查技巧实录

6.1 启动类报错:端口被占用

这个问题几乎每个开发者都会遇到。提示信息一般是Web server failed to start. Port 8080 was already in use.

排查思路很简单:在命令行执行netstat -ano | findstr 8080,找到占用 8080 端口的进程 PID,然后在任务管理器里结束这个进程。或者更省事的方法——直接改后端的端口配置,比如改成 8085,但要把前端的vue.config.js代理目标同步改掉,否则前端发请求会失败。

6.2 前端请求报错:Access to XMLHttpRequest... CORS

这个报错本质是跨域问题。单独开发前端、单独开发后端时各自都正常,一联调就报跨域,原因就是浏览器安全策略拦截了不同源(端口不同)的请求。

解决办法我优先推荐用 Webpack 的 devServer 代理,而不是在后端加@CrossOrigin。因为代理方案只在开发环境生效,生产环境把前端打包后放在同一域名下就没有跨域问题了。后端加跨域注解虽然开发时省事,但上线后如果前后端不在同一域名,还得靠 Nginx 的proxy_pass来配跨域,反而多了一层隐患。

6.3 数据库连接报错:Communications link failure

这个报错的常见原因有三个:一是 MySQL 服务没启动;二是连接 URL 里的 host、port、database name 写错了;三是账号权限不够。

我的排查顺序是:先确认 MySQL 服务在系统服务列表里处于运行状态;再检查application.yml里的连接串,尤其注意serverTimezone=Asia/Shanghai这个参数不能少;最后在 Navicat 里用同样的账号连接一次,如果 Navicat 能连上而后端连不上,就要检查是不是驱动版本跟 MySQL 版本不兼容。

6.4 高频问题速查表

问题现象排查方向解决方案
后端启动慢是否在初始化大量数据确认alwaysUpdateSsl设置,必要时关闭 SSL 校验
前端页面白屏控制台是否报 JS 错误检查路由配置和组件路径,确认没有循环引用
登录后跳回登录页拦截器是否放行了登录接口正确的做法是登录和注册接口必须放行,其他均拦截
图片上传失败文件路径配置是否正确检查本地存储路径是否存在且可写,或改用云存储
分页数据错乱SQL 里 limit 参数是否传对每次翻页都要把 page、pageSize 两个参数传给后端

7. 项目迭代与扩展的几点建议

系统跑通只是第一步,真正有价值的是它能不能在真实场景里落地。我根据自己的使用体验,列出几个后续可以迭代的方向。

第一个是可考虑接入真实支付渠道。目前系统里的支付是模拟流程,如果要在实际运营中使用,需要接入微信支付或支付宝支付。接入方式其实就是在OrderServiceImpl的支付方法里,把原来“直接改状态”的逻辑替换成调用第三方支付 SDK 的下单接口,回调地址里再写一个处理支付结果的方法。

第二个是增加数据可视化大屏。古城景区管理者的痛点是数据分散在各张表里,没有一个总览。可以做一个大屏页面,用 ECharts 图表展示今日售票数、实时游客量、热门景点排行、近 30 天营收趋势这些指标。数据从后端新增一个/api/statistics/overview接口返回聚合结果即可。

第三个是引入 Redis 做缓存。目前整个系统没有加缓存,高峰期景点详情页如果被大量访问,数据库压力会比较大。把热门的景点信息缓存到 Redis,设置 5 分钟的过期时间,数据库压力至少能减少一半。

我个人在实际开发里最深的一点体会是:这种管理系统的开发难度不在技术,而在需求梳理和业务流程建模。很多新手一上来就闷头写代码,等到联调阶段才发现字段没有对齐、状态流转有漏洞,反复返工。所以如果你要用这套代码作为自己项目的起点,建议第一个星期不要碰代码,先把表结构和业务流程彻底吃透。

最后分享一个小技巧:数据库脚本里我已经预置了足量的测试数据,包括十几个景点和几十条订单记录,方便你直接演示和调试。但如果你想真正理解这套系统,建议你亲手往里加两条数据和改一处逻辑,通过“自己动手弄脏代码”的方式,你掌握的东西会比单纯运行起来深刻得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询