SpringBoot+Vue前后端分离、Java+MySQL,这几个词组合在一起,基本就是当下毕业设计、课程设计项目里的标准配置。这次要聊的这套桂林旅游景点导游平台管理平台源码,正好是一个把后端接口、前端页面、数据库表全部串起来的完整项目,适合拿来当毕设、课设的底子,也适合刚接触全栈开发的同学逐行研究。
这套项目承载的业务很明确:景点信息展示、导游讲解预约、后台管理、用户管理一整套流程。听起来不复杂,但麻雀虽小五脏俱全,从前端的 Vue 路由,到后端的 JWT 登录认证,再到 MySQL 的表设计,几乎覆盖了一个 Web 应用要经历的各个环节。下面我按自己平时拆项目的习惯,把它的架构思路、技术选型、表设计、运行步骤和可复现的实操经验一条一条写清楚。
1. 项目整体思路与功能拆解
1.1 为什么选 SpringBoot + Vue 这套组合
先说结论:这套组合不是唯一选择,却是当前 Java 全栈项目里性价比最高、最容易被答辩老师认可的组合之一。
SpringBoot 解决的是后端开发体验问题。传统 SSM 项目要写大量 XML 配置,一个数据源配半小时,SpringBoot 用自动配置把这些杂事全接管了,写一个 Controller 就能直接跑起来。Vue 解决的是前端开发效率问题,组件化开发让页面复用变得很简单,配合 Element UI 这种现成组件库,后台管理界面半天就能搭出框架。两端通过 JSON 交互,职责清晰,代码结构一目了然。
从学习角度讲,这套组合还有一个隐性好处:市面上教程极多,无论是 B 站视频、博客文章还是 Gitee 上的开源项目,遇到问题基本都能搜到答案。相比之下,如果选一些冷门框架,光环境配置就够折腾好几天的。对于毕设这种有明确时间节点的任务,选成熟技术栈本身就是一种风险管理。
1.2 平台功能模块全景
我把这套平台按使用者拆成三块来看:
游客端(前台页面):注册登录、景点分类浏览、景点关键词搜索、景点详情查看、导游列表查看、在线预约导游讲解、发表评论、个人中心查看我的预约记录。这部分面向普通用户,重点在界面友好和操作顺畅。
管理端(后台页面):景点管理(新增、编辑、删除、上架下架)、景点分类管理、导游信息管理、预约订单审核、用户管理、系统数据统计。这部分面向管理员,重点在信息维护效率和数据结构完整性。
导游侧(简化后台):查看自己被预约的订单、确认或拒绝预约、维护个人简介和擅长语言。部分项目还会把导游侧并入管理端,用角色权限区分,具体看实现粒度。
这三块业务全部通过 RESTful 接口打通,前端只负责渲染数据,后端只负责提供数据和校验权限。所以你在源码里会看到很明显的分层:Vue 的 views 目录对应着页面的粒度,Java 的 controller/service/mapper 目录对应着业务逻辑的粒度。
1.3 角色划分与权限边界
角色设计是这类平台的核心,也是答辩时的加分项。通常分为三种角色:
- ROLE_USER 普通游客:只能访问前台页面,可以预约导游、发表评论、维护自己的个人信息。
- ROLE_ADMIN 管理员:拥有全部后台权限,可以管理景点、导游、预约审核和用户。
- ROLE_GUIDE 导游:可以查看与自己相关的预约记录,对预约进行确认操作。
权限控制必须做两层。前端用 Vue Router 的路由守卫控制页面访问,未登录用户跳转登录页,非管理员访问后台时拦截并提示;后端再通过拦截器校验每个请求携带的 Token,并在 Controller 里做角色判断。前端控制只是体验,后端控制才是安全底线,这一点是很多课设项目容易忽略的。
2. 技术栈选型与核心原理
2.1 后端选型:SpringBoot、MyBatis-Plus、MySQL
后端框架我建议直接用 SpringBoot 2.7.x,配 JDK 1.8 或 8+。有人会问为什么不上 Spring Boot 3,因为 3.x 强制要求 JDK 17,很多教学环境和企业生产环境还在用 JDK 8,选 2.7.x 兼容性最好,部署时也不用折腾。
数据库访问层首选 MyBatis-Plus。它把单表 CRUD 操作封装好了,Service 里写一行list(new LambdaQueryWrapper<>(Spot.class).like(Spot::getName, keyword))就能完成模糊查询,不用手写 XML SQL。遇到需要手写 SQL 的复杂统计场景,又可以直接在 Mapper 接口上写@Select注解,灵活度完全保留。相比之下,Spring Data JPA 虽然也能做,但碰到分页和动态查询时,HQL 和 JPA Specification 的学习成本比 MyBatis 的 XML 高不少,答辩时也容易被追问细节。
数据库用 MySQL 5.7 或 8.0 都可以。需要注意版本差异,MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,而且连接串里必须加时区参数serverTimezone=Asia/Shanghai,否则会报时区错误。5.7 虽然不用加,但加上也不影响。
依赖清单里还会看到 Lombok(省去 getter/setter 模板代码)、Hutool(封装了常用工具类如加密、日期处理)、JWT 相关库(用于登录令牌)。这三件套基本是这类项目的标配。
2.2 前端选型:Vue 2 + Element UI + Axios
前端框架选 Vue 2.x 还是 Vue 3.x,要分情况讨论。如果是从零开始写新项目,我会推荐 Vue 3 + Vite + Element Plus,性能更好、组合式 API 更现代;但如果是参照现成源码改,且源码本身是 Vue 2 + Element UI,那就没必要强行升级,因为升级不是简单改版本,而是连带改组件用法、生命周期和路由写法,工作量很大。
这套源码沿用的是 Vue 2 生态。Vue 2 配 Element UI 的优势是稳定、资料多、组件封装完整,后台管理系统的表格、表单、弹窗、分页组件拿来即用。Axios 负责 HTTP 请求,通过拦截器统一注入 Token 和处理返回值,不用在每个页面里重复写请求逻辑。
Vue Router 做页面跳转,模式建议选hash模式,因为history模式刷新页面时若不配置服务器重定向会 404,有时候部署环境不支持配置,直接 404 卡在登录页。虽然 hash 模式地址栏多个#不太好看,但胜在省心,对毕设演示项目是最稳的选择。
2.3 前后端交互的规范与认证机制
前后端交互靠的是一套约定俗成的 JSON 格式。后端统一返回结构大致是:
public class Result<T> { private Integer code; // 200 成功,500 失败,401 未登录 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "操作成功"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }前端 Axios 拦截器这样写:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) axios.interceptors.response.use( res => { const data = res.data if (data.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject('未登录或登录过期') } return data }, err => Promise.reject(err) )登录成功后,后端签发一个 JWT 字符串,前端存到localStorage,之后所有请求都自动带上。后端的拦截器拿到 Token 后做两件事:先校验签名和有效期,再解析出用户 ID 和角色,放到当前请求的上下文里,供 Controller 取用。这样一个无状态认证流程就闭环了。
3. 数据库设计与核心表结构
3.1 表结构设计的基本思路
数据库设计直接决定了业务代码好不好写。我的习惯是先画业务关系图,再转成表结构:一个用户可以对多个景点发表多条评论,一个用户可以提交多个预约单,一个景点可以对应多个导游,一个景点属于一个分类。其中预约表是核心桥接表,把用户、景点、导游三者的关系串起来。
表命名统一用下划线风格,Java 实体类用驼峰命名,配置 MyBatis-Plus 的驼峰映射后自动转换。主键统一用bigint的自增 ID,业务字段尽量用varchar和datetime,金额字段用decimal(10,2)。每个业务表都带create_time和update_time两个通用字段,插入和更新时交给公共字段自动填充,省去每个方法里手动赋值。
3.2 核心业务表逐表拆解
这里列六张核心表,基本能覆盖项目全部业务:
用户表 sys_user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录账号,唯一索引 |
| password | varchar(100) | BCrypt 加密后的密文 |
| nickname | varchar(50) | 昵称 |
| phone | varchar(20) | 手机号 |
| avatar | varchar(255) | 头像地址 |
| role | varchar(20) | 角色编码 user/admin/guide |
| status | tinyint | 状态 1正常 0禁用 |
| create_time | datetime | 创建时间 |
密码字段这一条要特别强调:不要明文存储。项目里用 Spring Security 的BCryptPasswordEncoder或 Hutool 的 BCrypt 工具加密,登录时再比对密文,既安全也是答辩加分项。
景点分类表 spot_category
字段包括 id、name、sort、status。分类一般就做两层,比如“自然风光”“人文历史”“休闲度假”,前端列表页用分类筛选景点。
景点表 scenic_spot
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| category_id | bigint | 分类ID,外键逻辑关联 |
| name | varchar(100) | 景点名称 |
| cover | varchar(255) | 封面图 |
| images | text | 轮播图,逗号分隔多个URL |
| address | varchar(255) | 地址 |
| description | text | 详细介绍 |
| price | decimal(10,2) | 门票参考价 |
| heat | int | 热度,用于排序 |
| status | tinyint | 上架状态 |
| create_time | datetime | 创建时间 |
导游表 guide
字段包括 id、name、avatar、introduce、language(擅长语种)、level(星级)、status。有些项目会把导游和用户表打通,用user_id字段关联,导游信息挂在用户账号下,这样导游登录时能直接看到自己的订单。推荐后一种设计,逻辑更合理。
预约表 reservation
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 预约人 |
| spot_id | bigint | 关联景点 |
| guide_id | bigint | 导游 |
| tour_date | date | 讲解日期 |
| status | tinyint | 0待确认 1已确认 2已完成 3已取消 |
| remark | varchar(255) | 备注 |
| create_time | datetime | 预约时间 |
评论表 comment
字段包括 id、user_id、spot_id、content、score、create_time。评分可以做成星标组件,前端富文本和评分器直接绑定,后端只存整数或小数点后一位。
3.3 容易踩坑的字段设计细节
第一,状态字段的语义一定要统一。比如预约的 status 用 0/1/2/3,那我建议在前后端约定成常量字典,前端用this.$dict或枚举映射显示中文,而不是散布在代码各处的魔法数字,否则后期维护时根本分不清 2 到底是“已完成”还是“已取消”。
第二,金额不能存 float。浮点数有精度问题,票务类数据一旦涉及金额,务必用 decimal。
第三,图片存储不要直接存二进制。本地开发时把图片放到项目静态目录,存相对路径;部署后建议用对象存储,表里存 URL。这样既简单又不会拖慢数据库。
第四,给查询频繁的字段加索引。景点表的 name 加普通索引,预约表在 user_id 和 status 上加联合索引,评论表在 spot_id 上加普通索引。数据量小的时候不明显,但如果答辩时被问到性能优化,这个回答比“我加了缓存”更有说服力。
4. 环境搭建与项目初始化实操
4.1 后端环境准备与依赖
后端运行需要三样东西:JDK 1.8 或 8+、Maven 3.6+、IDEA 或 Eclipse。安装时注意JAVA_HOME环境变量要配到 JDK 的根目录,Maven 的settings.xml里最好配置国内镜像源,不然下载依赖能慢到怀疑人生。
核心 pom.xml 依赖片段如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> </dependencies>spring-boot-starter-web是基础中的基础,负责内嵌 Tomcat 和 MVC 相关能力;MyBatis-Plus 负责操作数据库;Lombok 减少样板代码;jjwt 和 hutool 负责 Token 生成和常用工具。
4.2 前端环境准备与依赖
前端需要 Node.js,版本 14 或 16 都可以,不建议直接上最新的 20/22,部分旧依赖可能和 Node 20 有兼容问题。装完 Node 后自带 npm,建议再配一个淘宝镜像源:
npm config set registry https://registry.npmmirror.com然后进入源码的 frontend 目录执行npm install。如果安装过程报错,优先检查 Node 版本和镜像源,其次删掉node_modules重新安装,大多数问题都能这样解决。
4.3 从零到一:把整套源码跑起来
拿到源码后按这个顺序操作:
- 创建数据库,比如
guilin_tour,导入项目根目录下的sql文件,初始化表结构和基础数据。 - 用 IDEA 导入后端代码,等待 Maven 下载完依赖。
- 修改
application.yml里的数据库连接信息:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/guilin_tour?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true- 运行后端主类,看到 Tomcat started 说明后端起来了。
- 打开前端目录,执行
npm install完成后执行npm run serve,默认端口是 8081,浏览器访问http://localhost:8081。
前端起在 8081,后端在 8080,前端通过vue.config.js里的 devServer 代理把/api开头的请求转发到后端,从而绕过跨域问题。这个代理配置是前后端联调的关键,我见过不少同学前端请求直接写http://localhost:8080,虽然也能通,但一部署就残废,不如一开始就走代理。
4.4 二次开发的几种切入方式
拿到源码后不建议上来就改功能,先按三条线熟悉项目:
第一条线是“接口线”,打开后端 Controller,逐个看每个接口的 URL、入参、返回值,对应前端src/api目录下的请求方法。第二条线是“数据库线”,对照 3.2 节的表结构,理解每张表的数据流向。第三条线是“页面线”,打开前端 views 目录,从登录页开始走一遍,看每个页面调用了哪些接口。
如果要新增功能,比如给景点加一个“推荐指数”字段,按这个顺序改:先给scenic_spot表加字段,再在实体类加属性,再用 MyBatis-Plus 的代码生成器或者手写 Service 方法,最后在前端列表页和详情页补上展示。全程不用大改框架,这也是这类项目适合二次开发的重要原因。
5. 核心业务实现:关键模块走一遍
5.1 景点模块:列表分页、搜索、详情
景点模块是最基础也最完整的 CRUD 示例。前端列表页通过pageNum、pageSize、keyword、categoryId四个参数请求接口,后端 Controller 接收后传给 Service:
@GetMapping("/spot/list") public Result<IPage<ScenicSpot>> list(@RequestParam Integer pageNum, @RequestParam Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { LambdaQueryWrapper<ScenicSpot> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), ScenicSpot::getName, keyword) .eq(categoryId != null, ScenicSpot::getCategoryId, categoryId) .orderByDesc(ScenicSpot::getHeat); IPage<ScenicSpot> page = spotService.page(new Page<>(pageNum, pageSize), wrapper); return Result.success(page); }这段代码演示了 MyBatis-Plus 最常用的两点:LambdaQueryWrapper链式条件构造,以及like和eq配合布尔参数实现“条件动态生效”。关键词搜索用LIKE,分类筛选用EQ,排序按热度倒序,前台展示天然带热门排序效果。
详情页就简单了,直接按 ID 查一条,同时查该景点的评论列表和可选导游列表。三个接口并行返回,前端用Promise.all请求,减少白屏时间。
5.2 导游预约:一张表串起完整业务闭环
预约是整个平台最有业务含量的模块。游客在景点详情页选择导游、日期、填写备注,提交后生成reservation记录,状态为 0(待确认)。管理员或导游在后台看到待确认列表,点击确认后状态变为 1(已确认),游客端个人中心就能看到“已确认”标识。到约定日期后,管理员在后台标记“已完成”,完成整个闭环。
创建预约时要处理几个边界情况:同一个导游同一日期不能重复预约,同一个用户对同一个景点不能同时存在未完成预约,这两个校验一个写在 SQL 层,一个写在 Service 层:
long count = reservationService.count(new LambdaQueryWrapper<Reservation>() .eq(Reservation::getGuideId, guideId) .eq(Reservation::getTourDate, tourDate) .ne(Reservation::getStatus, 3)); if (count > 0) { return Result.error("该导游当天已有预约安排"); }这段写在哪里很重要。写在 Controller 里是“面向过程”,写在 Service 里才对,因为这属于业务规则,不属于接口适配逻辑。
5.3 登录鉴权:JWT 是核心
登录接口的流程是:前端提交用户名和密码,后端查出用户,用BCrypt校验密码,通过后生成 JWT 返回前端。
JWT 生成的核心逻辑:
public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }我设置的是 7 天有效期,毕设演示足够。如果希望更严谨,可以引入 Redis 存 Token 并做单点登录,过期时间是半小时。答辩时如果你能说出“无状态认证”这几个字,老师通常会觉得你是真理解了,而不是只会调登录接口。
注意,真正实现时 Controller 层代码不要自己解析 Token,统一在拦截器里解析,然后通过ThreadLocal或方法参数传给业务层,这样业务代码不用关心认证细节,职责更清晰。
5.4 管理端的统计数据怎么来
后台首页通常要展示平台概况:总用户数、景点数、预约总数、近七天新增预约。这类数据适合在 MySQL 里直接聚合:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS cnt FROM reservation WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day在 MyBatis 的 Mapper 中写@Select注解,返回结果用一个包含day和cnt的视图对象接收,前端用 ECharts 折线图展示。整个过程不难,但写 SQL 时注意DATE_FORMAT和GROUP BY的字段要对齐,否则分组结果不对。
6. 常见问题与排查技巧实录
6.1 高频报错速查表
我整理了一份这套项目运行时最常见的报错和排查方向,照着查基本能解决 80% 的问题。
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| 启动报 Access denied for user | 数据库账号密码错误,或 root 不支持远程 | 检查application.yml的用户名密码,本地用 root/密码连一下 navicat |
| 连接 MySQL 报时区错误 | MySQL 8 连接串缺少时区参数 | 连接串加serverTimezone=Asia/Shanghai |
| 前端请求 404 | 代理没生效,或接口路径不对 | 先直接访问后端接口确认存在,再检查vue.config.js的 devServer 代理 |
| 前端跨域报错 | 前后端端口域名不一致且没走代理 | 用 devServer 代理,不要在前端写死后端地址 |
| 8080 端口被占用 | 其他程序占用了端口 | 命令行执行netstat -ano | findstr 8080,结束对应进程,或改 server.port |
| Maven 依赖下载失败 | 仓库源太慢或依赖冲突 | 配置阿里云镜像源,删除本地.m2/repository下对应目录重新拉取 |
| npm install 一直报错 | Node 版本过新或网络问题 | 切换 Node 14/16,配置 npmmirror 源 |
| 页面白屏,控制台报 favicon 404 | 前端资源路径问题 | 检查publicPath配置,一般是/或相对路径 |
| 中文乱码 | 数据库连接串或表编码问题 | 建库时指定 utf8mb4,连接串加characterEncoding=utf8 |
6.2 容易丢分的细节清单
项目能跑只是及格线,能不能高分看细节。我评项目时最常看到这几个问题:
第一,没有任何异常处理。后端代码到处 try-catch 但都空着,或者没捕获时直接把堆栈抛给前端,页面上一大段英文报错。正确做法是用@RestControllerAdvice全局异常通知,统一记录日志并转换为友好提示。这是生产级和玩具项目的分水岭。
第二,密码明文存库。这个问题严重到可以直接影响成绩。记得用 BCrypt 加密,实在不想用 Spring Security 全家桶,用 Hutool 的 BCrypt 工具也行,关键是库里的密文不能一眼看出原密码。
第三,删除数据直接DELETE。管理端删除景点时,物理删除会把关联的预约和评论一起删掉,数据完整性被破坏。用逻辑删除字段deleted配合 MyBatis-Plus 的@TableLogic注解,删除只是打个标记,数据还在,随时能恢复。
第四,分页查询写死LIMIT 页码。手写 SQL 分页时不要自己拼 offset,用 MyBatis-Plus 的Page对象或者 MyBatis 的 PageHelper,既安全又省事。
第五,上传图片的路径写死。比如把图片保存到本地磁盘某目录,页面用C:\xxx绝对路径访问,换台电脑就裂开。要么存项目静态目录相对路径,要么存对象存储 URL。
6.3 被答辩老师问住的几个技术问题
我模拟一下答辩现场,这几个问题被问概率极高:
为什么用 MyBatis-Plus 不用 JPA?答:本系统涉及较多动态条件查询和聚合统计,MyBatis-Plus 既保留了手写 SQL 的灵活性,又对单表 CRUD 做了封装,开发效率高;JPA 在简单 CRUD 上更方便,但多表动态查询的学习成本和书写成本偏高。
前端路由守卫是干嘛的?答:在进入每个页面之前做校验,比如未登录跳转登录页,非管理员访问后台提示无权限。它是权限提示层,真正防越权靠后端的 Token 校验和角色判断。
Token 被别人盗用了怎么办?答:Token 存在前端 localStorage 里,理论上存在被 XSS 窃取的风险。可以在密码修改时让 Token 立即失效,在服务端维护一个黑名单列表,或者把有效期设置短一些,再配合刷新令牌机制。毕设项目做到 7 天有效期和统一过期处理,已经能满足需求。
如果景点数据量过百万,当前表设计有什么问题?答:热点查询景点名和分类,可以用覆盖索引减少回表;评论和预约按景点 ID 分库分表;景点详情这种大文本字段单独拆表或走缓存。这题没有标准答案,重点是展示你有扩展意识,而不是只会写 CRUD。
项目中哪个模块是你独立完成的?答:提前准备一个完整的模块讲清楚,比如导游预约模块,从建表、查询、状态流转到前端交互,能把细节说的越具体越好。老师问这个问题,是判断你对自己的项目到底了解多深。
最后再分享一个实操技巧
这套项目我前后带人跑过好几遍,最深的体会是:不要一上手就摊开所有代码,先只启动后端,用 Apifox 或 Postman 把登录、景点列表、预约这三个核心接口调通,再去开前端页面。这样做的好处是,一旦前端报错,你能立刻判断问题出在前端还是接口层,而不是被一个报错信息带着满项目乱翻。
还有一个小诀窍,如果你时间紧张,加需求不要硬造模块,优先找现有模块的“毕业缺陷”补上。比如给景点加多图轮播、给评论加回复、给预约加导出 Excel。这些功能看似零碎,但每一个都能讲出完整业务闭环,在课设和毕设报告里特别好写,改造成本也低。
最后提醒一句:网上很多源码下载下来根本跑不通,大概率不是代码问题,而是环境问题。按我上面 4.3 节的顺序一步一步走,本地能跑通再谈改功能。祝你的毕设顺利。