资讯动态

SpringBoot旅游管理系统项目实战:从数据库设计到JWT鉴权与部署

发布时间:2026/9/16 15:13:17 来源:尧图企业网站定制
简介这套SpringBoot旅游管理系统项目资料面向计算机专业学生与Java后端开发者可作为毕业设计、课程设计或期末大作业的完整参考。系统围绕旅游业务展开涵盖旅游线路查询、酒店预订、景点信息展示、旅游攻略分享等模块并配置用户管理、订单管理、支付接口集成、评论推荐等常见功能流程。压缩包内共730个文件约21.44MB代码层面以90个Java类、38个Vue组件、152个JS脚本、44个CSS样式为主辅以162个SVG图标、79个GIF动图同时提供SQL数据库脚本、XML配置、说明文档及一键启动脚本导入IDE即可运行。目前已有58人学习下载。整套资料包含可运行源码、数据库设计文档与论文文档能帮助读者理解Spring Boot分层架构、MyBatis持久化、前后端分离开发等关键知识点前端组件与后端接口一一对应适合用于项目实战、代码研读、课设答辩或二次开发。1. SpringBoot旅游管理系统毕设季最常被问到的那个项目到底该怎么做每年毕设季SpringBoot旅游管理系统都是 JavaWeb 方向里出现频率最高的题目之一。旅游线路、门票、订单、评论这些模块天然适合用来演示 SpringBoot 的分层开发和数据库设计能力也正因为如此网上能找到的源码质量差别极大——有的版本老到要改 JDK 才能跑有的给了源码但数据库脚本残缺还有的论文与代码根本对不上号。这篇顺着实现的完整链路讲一遍从技术选型、数据库设计到 JWT 鉴权和下单逻辑最后落在源码怎么初始化、论文怎么写才能和代码一一对应。目标很直接你手里已有源码按这套思路能快速跑通你想从零写也能按这套骨架把项目立起来。2. 技术选型与工程分层先把 SpringBoot 旅游管理系统的地基打稳2.1 版本选择为什么我把 SpringBoot 2.7.x 作为默认答案SpringBoot 3.x 已经发布很久JDK 17 也成了主流但遇到旅游管理系统这类以毕业设计、课程设计为目标的 SpringBoot 项目我一般会建议锁SpringBoot 2.7.18 JDK 8/11。原因很实际网上流传的教程和开源源码绝大多数基于 2.x同样的报错在 2.x 下能搜到完整解决方案2.7 是 2.x 系列的最终维护版本安全补丁齐全部署到云服务器时2.x 对 Docker 镜像和系统自带 JDK 的兼容性也最省事。这不是说 3.x 不好而是这个场景下稳定性优先。组件推荐选型关键说明JDK1.8 / 112.7.x 完全兼容JVM 参数按网上常见教程配置即可SpringBoot2.7.182.x 最终版本教程量与兼容性最平衡ORMMyBatis-Plus 3.5.x单表 CRUD 几乎不用写 SQL分页插件成熟数据库MySQL 8.0 / 5.7注意 MySQL 8.x 驱动类名是 com.mysql.cj.jdbc.Driver权限方案JWT 拦截器无状态答辩时好讲清楚不需要引入 Spring Security 全家桶前端Thymeleaf 或 Vue 分离看团队水平本篇以 Thymeleaf Bootstrap 这种单体内嵌方式为例2.2 pom.xml 里最常用的几组依赖和版本注解parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 启动器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus注意 groupId 是 com.baomidou-- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 8 驱动5.7 数据库也能用这个驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- JWT 工具包 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- Lombok 减少实体类模板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段依赖里最需要注意的是 mybatis-plus-boot-starter 的 groupId我见过不少人抄成 mybatis-plus 官方文档里的com.baomidou以外的坐标导致依赖拉不下来。jjwt 0.9.1 是兼容 JDK 8 的老版本新项目用 0.11.x 的话 API 变动较大Jwts.builder()的写法完全不同。2.3 工程分包一个 JavaWeb 旅游项目最容易被轻视的部分com.travel ├── TravelApplication.java ├── common // 统一返回结构 Result、异常处理 ├── config // 拦截器、WebMvcConfigurer、MyBatisPlus 分页配置 ├── controller // 前端接口 ├── entity // 数据库实体 ├── mapper // MyBatis-Plus Mapper 接口 ├── service // 业务接口 │ └── impl // 业务实现 └── utils // JWT、日期、字符串工具很多源码拿到手跑不起来不是代码错而是包名和启动类位置对不上。SpringBoot 默认扫描启动类所在包及子包如果把 controller 放在com.controller这类与启动类不相关的包名里项目能启动但接口 404。旅游管理系统这个体量按上面这套分包足够controller 只做参数接收和结果返回service 写业务逻辑mapper 负责数据库交互。2.4 application.yml 里最值得注意的三处 springboot 配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0三处容易踩坑的细节第一serverTimezone不配或配成 UTC数据库查询出来的时间会比北京时间早 8 小时这在订单时间展示时特别明显第二map-underscore-to-camel-case为 true 时数据库字段created_at才能自动映射到实体的createdAt不配的话查出来全是 null第三逻辑删除配置是 MyBatis-Plus 3.x 推荐的做法删除操作变成 update避免旅游景点下架后关联订单数据被物理删除。3. 旅游管理系统的数据库设计核心表结构与关联关系3.1 业务表拆解从需求分析到六张核心表旅游管理系统无论叫“旅游网站”还是“智慧旅游平台”核心业务链都是用户浏览景点和线路 → 收藏感兴趣的景点 → 下单购买门票或线路 → 支付后生成订单 → 在个人中心查看订单和发表评论。映射到数据库就是六张表用户表、景点表、线路表、订单表、评论表、收藏表。其中线路表在国内很多毕业设计里是包含“行程天数、出发地、目的地”的大字段表而订单表的字段设计是最影响后续开发的。表名职责与其它表的关系sys_user用户与管理员共表role 字段区分被订单、评论、收藏关联scenic_spot景点基本信息与票价被订单、评论、收藏关联travel_route旅游线路含交通和行程描述属于某个景点或跨景点orders订单主表保存下单快照关联用户和景点comment用户对景点/线路的评价关联用户和景点favorite收藏关系表关联用户和景点3.2 用户表和景点表的建表 SQL 与索引说明CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(32) DEFAULT NULL COMMENT 昵称, phone VARCHAR(11) DEFAULT NULL COMMENT 手机号, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, role TINYINT NOT NULL DEFAULT 1 COMMENT 1普通用户 2管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_role (role) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE scenic_spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, name VARCHAR(64) NOT NULL COMMENT 景点名称, location VARCHAR(128) DEFAULT NULL COMMENT 所在地, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 门票价格, open_time VARCHAR(64) DEFAULT NULL COMMENT 开放时间, description TEXT COMMENT 景点介绍, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表;这里有两个决策值得在论文的设计部分写清楚。第一用户名加 UNIQUE 索引除了防重复更重要的是为登录查询提供索引登录场景占总查询量的比例非常高。第二景点表加status字段而不是物理删除这样订单关联的景点信息不会因为管理员的删除操作而丢失。密码字段用 VARCHAR(64) 是给 BCrypt 加密后的字符串准备的BCrypt 每次加密结果不同不要用 MD5 这种可逆破译的算法。3.3 订单表把快照字段冗余到订单里订单表是整个旅游管理系统里数据库设计最值得展开的表也是答辩时评委最常追问的地方。CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 下单用户ID, spot_id BIGINT NOT NULL COMMENT 景点ID, spot_name VARCHAR(64) NOT NULL COMMENT 景点名称快照, spot_price DECIMAL(10,2) NOT NULL COMMENT 下单时票价快照, quantity INT NOT NULL DEFAULT 1 COMMENT 购买数量, total_amount DECIMAL(10,2) NOT NULL COMMENT 总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已完成, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单表冗余spot_name和spot_price是故意设计的。景点名称修改了、价格调整了历史订单仍然保留用户下单时的快照数据这就是“订单快照”概念。如果查询订单时要 JOIN 景点表取名称和价格一旦景点被下架或删除用户的历史订单就会显示异常。数据库课程设计和实际项目中订单类表必须冗余业务快照字段。3.4 多对多关系收藏和评论表的建模用户和景点是多对多关系通过收藏表和评论表来表达这种关系。CREATE TABLE favorite ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_spot (user_id, spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; CREATE TABLE comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL COMMENT 评论内容, score TINYINT DEFAULT 5 COMMENT 评分 1-5, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_spot_id (spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;收藏表用UNIQUE KEY uk_user_spot (user_id, spot_id)做联合唯一索引直接杜绝同一用户重复收藏同一景点的脏数据代码里就不需要先查再插了。评论表只保留用户 ID、景点 ID 和内容字段用户昵称、头像在展示时通过 JOIN 查询获取——注意这里不需要快照因为评论展示的是即时数据用户改昵称后评论里跟着变也是合理行为。4. 核心模块实现鉴权、下单与管理端的 SpringBoot 代码落实4.1 基于 JWT 的登录鉴权怎么落地到 SpringBoot 项目旅游管理系统的用户端和管理端通常共用一个后端服务通过 JWT 里携带的角色信息区分权限。JWT 方案比 Session 更适合这种前后端交互场景用户登录后得到一个 token前端存到 localStorage每次请求放到 Authorization 头里后端拦截器解析 token 并放行。public class JwtUtil { // 正式项目中密钥从配置文件读取不要硬编码 private static final String SECRET travel-project-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 24; // 24小时 public static String createToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }生成 token 时把 userId 和 role 放进 claim解析时直接从 Claims 里取省去每次请求都查数据库检索用户的步骤。这个工具类在源码里属于高频复用类登录接口、拦截器都要用到。注意事项是 jjwt 0.9.1 的parseClaimsJws在 token 过期或签名错误时会抛异常所以拦截器里必须 try-catch。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求否则前端跨域调用会失败 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { // token 过期、被篡改或签名错误统一返回 401 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }拦截器里解析出的 userId 放进 request attributecontroller 里用RequestAttribute(userId) Long userId就能拿到当前登录用户非常顺手。注册拦截器时用addPathPatterns(/api/**)拦截所有接口用excludePathPatterns(/api/login, /api/register, /api/scenic/**)放行登录注册和景点浏览接口——景点列表是游客也能看的这个放行逻辑是源码里常见但容易被忽略的一步。4.2 下单接口的事务处理与防重复提交下单是旅游管理系统最核心的写操作涉及读景点信息、计算金额、写订单表、扣减库存如果做库存功能这几个步骤必须在一个事务里。我在实际写这块代码时会在类上直接标注Transactional(rollbackFor Exception.class)明确指定异常时回滚。Service RequiredArgsConstructor public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService { private final ScenicSpotMapper spotMapper; Override Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long spotId, Integer quantity) { ScenicSpot spot spotMapper.selectById(spotId); if (spot null || spot.getStatus() ! 1) { throw new RuntimeException(景点不存在或已下架); } // 业务前置校验购买数量必须大于 0 if (quantity null || quantity 0) { throw new RuntimeException(购买数量不合法); } // 订单号时间戳 用户ID 随机数避免并发下重复 String orderNo System.currentTimeMillis() userId ThreadLocalRandom.current().nextInt(1000, 9999); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setSpotId(spotId); order.setSpotName(spot.getName()); order.setSpotPrice(spot.getPrice()); order.setQuantity(quantity); order.setTotalAmount(spot.getPrice().multiply(new BigDecimal(quantity))); order.setStatus(0); this.save(order); return OrderVO.from(order); } }下单逻辑的关键在于把“查景点状态校验”和“保存订单”放进同一个事务。没有事务时会出现景点管理员刚下架了景点用户这边还在正常下单的边界问题。rollbackFor Exception.class指定了任何异常都触发回滚不加这个参数时 Spring 默认只在 RuntimeException 时回滚检查异常会导致数据半写入。订单号用时间戳加随机数在毕业设计级别够用生产系统需要专门的发号器但论文里写清楚这个方案的理由就足以应付答辩。4.3 管理端的通用 CRUD 套路MyBatis-Plus 让数据库增删改查变薄景点管理、线路管理这些管理端模块本质都是对单表做增删改查加条件分页。MyBatis-Plus 的 IService 和 ServiceImpl 已经把这层逻辑封装好了我需要做的只是定义查询条件。public interface ScenicSpotService extends IServiceScenicSpot { PageScenicSpot pageQuery(int current, int size, String keyword); } Service public class ScenicSpotServiceImpl extends ServiceImplScenicSpotMapper, ScenicSpot implements ScenicSpotService { Override public PageScenicSpot pageQuery(int current, int size, String keyword) { LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); // 关键字模糊搜索名称或所在地 if (StringUtils.hasText(keyword)) { wrapper.like(ScenicSpot::getName, keyword) .or().like(ScenicSpot::getLocation, keyword); } wrapper.orderByDesc(ScenicSpot::getCreatedAt); return this.page(new Page(current, size), wrapper); } }这段代码把查询构造从 XML 里解放出来LambdaQueryWrapper 用方法引用指字段编译期就能发现字段名拼写错误。分页插件需要单独配置一个 MybatisPlusInterceptor BeanPaginationInnerInterceptor的数据库类型要设成DbType.MYSQL不配置的话page方法会查出全部数据再在内存里截断数据一多就出问题。这个是源码项目里最常见的配置缺失点项目能跑但分页无效答辩演示时一页显示全部记录就很尴尬。RestController RequestMapping(/api/admin/scenic) RequiredArgsConstructor public class ScenicSpotAdminController { private final ScenicSpotService scenicSpotService; GetMapping(/page) public ResultPageScenicSpot page(RequestParam(defaultValue 1) int current, RequestParam(defaultValue 10) int size, RequestParam(required false) String keyword) { return Result.ok(scenicSpotService.pageQuery(current, size, keyword)); } PostMapping public ResultVoid save(RequestBody ScenicSpot spot) { scenicSpotService.save(spot); return Result.ok(); } PutMapping public ResultVoid update(RequestBody ScenicSpot spot) { scenicSpotService.updateById(spot); return Result.ok(); } DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { scenicSpotService.removeById(id); return Result.ok(); } }Controller 层保持瘦身只做参数接收和返回包装。注意新增和修改用的是两个不同 HTTP 方法但同一个路径前端做表单提交时不要搞混。逻辑删除配置生效后removeById执行的是 UPDATE 语句把 deleted 字段置 1不需要额外写删除 SQL。5. 源码到手之后数据库初始化、论文成稿与答辩自查的三个要点5.1 用最小路径跑通源码拿到源码后第一步不是启动项目而是初始化数据库。大部分旅游管理系统源码会附带travel.sql或tourism.sql用 Navicat 或命令行执行mysql -u root -p travel.sql脚本执行成功后会创建travel数据库和全部表。这时打开 application.yml核对三处数据库名是否和脚本里的库名一致、用户名密码是否和本地 MySQL 一致、serverTimezone是否配置。改完后执行mvn spring-boot:run或直接运行启动类。如果启动报错说数据库连接失败99% 是密码错误或者数据库名写错报错信息里出现Public Key Retrieval is not allowed就在 url 末尾加allowPublicKeyRetrievaltrue这是 MySQL 8 和驱动之间的老问题。5.2 论文里的图全部从源码运行结果里截SpringBoot 旅游管理系统的论文最怕出现“功能设计图漂亮但系统里根本没有这个功能”。我给一个可操作的办法论文里的功能模块图对照 controller 包的每个类来画删掉源码里不存在但论文里写了的模块E-R 图对照第 3 章的六张表来画字段以实际建表语句为准核心界面截图全部从本地运行的系统里截。接口层面用 Postman 或 Apifox 调一遍把请求和响应结果截图放进系统测试章节这比任何现成图都有说服力。5.3 答辩前必查的三个技术细节答辩时评委爱问细节三个点必查。第一MyBatis-Plus 分页插件是否配置没配的话列表页数据异常第二Transactional是否带了rollbackFor没带的话要能解释事务边界怎么控制第三管理员账号的初始密码和数据库里的加密值是否匹配很多源码默认密码是123456但里面存的是 BCrypt 密文你直接用明文登录会失败。把这三点提前验证一遍比背十道 springboot 面试题都管用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价