简介:这是一套基于SpringBoot开发的旅游出行指南系统完整源码,面向Java后端与全栈初学者,适用于课程设计、毕业设计及旅游类Web应用快速原型开发。系统采用B/S架构,前后端分离设计,涵盖微信小程序(UniApp/Vue)、SpringBoot后端、MySQL数据库及MyBatisPlus持久层,支持用户管理、图片与视频素材上传展示等核心功能。压缩包含721个文件,主体为187个Java业务逻辑类、131个Vue页面组件、161个SVG图标资源及74个JPG/36个PNG媒体素材,辅以JS交互脚本、CSS样式、YML配置及BAT启动脚本,总大小24.02MB,目录结构完整,含详细文档(如摘要、技术介绍、系统分析与实现章节)。目前已有79人学习下载,代码注释较充分,模块划分清晰,可直接导入IDE运行调试,是理解旅游服务类系统从需求分析到前后端联调落地的典型实践案例。
1. 一个能跑起来的旅游出行指南网站,为什么非得用 Spring Boot 而不是纯 Servlet 或 JSP?
你手头有一份标着“基于 SpringBoot 的旅游出行指南系统代码”的压缩包,解压后看到pom.xml里写着spring-boot-starter-web、spring-boot-starter-thymeleaf、spring-boot-starter-data-jpa,还有application.yml和一堆Controller类——这不是教学 Demo,而是一个真实可部署的轻量级旅游信息网站:支持景点分类浏览、城市筛选、图文详情页、用户收藏、后台管理入口。它不依赖 Tomcat 手动部署 WAR 包,也不用写web.xml配置过滤器链;mvn spring-boot:run启动后,http://localhost:8080就直接打开首页,连静态资源路径都自动映射好了。这类系统适合中小型旅行社做官网导流、高校课程设计展示、或自由开发者接单交付原型——它把 Java Web 开发中重复率最高的配置、路由、数据绑定、模板渲染全封装掉了,让你专注在「怎么展示九寨沟的开放时间」「如何按海拔排序高原景点」这些业务逻辑上。如果你还在用传统 Servlet 写doGet()处理 URL 参数、手动拼 HTML 字符串返回页面,那这份 Spring Boot 代码就是你跳过十年胶水代码的快捷通道。
2. 从空目录到首页可访问:Spring Boot 旅游指南系统的最小可运行骨架
2.1 初始化项目结构与核心依赖选择依据
新建 Maven 项目时,pom.xml必须包含三类基础 Starter:
spring-boot-starter-web提供内嵌 Tomcat 和 REST 接口能力,这是所有 Web 系统的底座;spring-boot-starter-thymeleaf替代 JSP 实现服务端模板渲染,它天然支持 HTML 原生语法,前端改.html文件无需重启,对旅游网站这种强内容展示型应用极其友好;spring-boot-starter-data-jpa封装 Hibernate,让景点、城市、攻略等实体用@Entity注解就能映射数据库,避免手写 JDBC 模板和 SQL 字符串拼接——尤其当你要查「云南大理下关区海拔低于2000米的4A级景区」时,JPQL 查询比原生 SQL 更易维护。
提示:不要引入
spring-boot-starter-security到初始版本。旅游指南是公开信息站,强行加登录拦截反而阻碍首页加载,等后台管理模块(如景点编辑)开发完成后再按需启用。
以下是最简pom.xml关键片段(Spring Boot 3.2.x 兼容 Jakarta EE 9+):
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.7</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>h2是内存数据库,开发阶段免去 MySQL 安装和建库操作,启动即用。正式部署时只需替换为mysql-connector-j并修改application.yml中的spring.datasource.url即可。
2.2 主启动类与全局配置文件的必填项
src/main/java/com/example/travel/TravelGuideApplication.java是整个系统的入口:
@SpringBootApplication public class TravelGuideApplication { public static void main(String[] args) { SpringApplication.run(TravelGuideApplication.class, args); } }@SpringBootApplication自动启用组件扫描、自动配置和 Spring MVC,无需额外 XML。但必须配合application.yml才能生效:
server: port: 8080 servlet: context-path: /travel spring: datasource: url: jdbc:h2:mem:traveldb driver-class-name: org.h2.Driver username: sa password: "" jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true properties: hibernate: format_sql: true thymeleaf: cache: false enabled: true prefix: classpath:/templates/ suffix: .html encoding: UTF-8 servlet: content-type: text/html关键参数说明:
server.servlet.context-path: /travel表示所有接口前缀为/travel/,避免与本地其他服务冲突;jpa.hibernate.ddl-auto: create-drop在每次启动时重建表结构,适合开发期快速迭代(上线必须改为validate);thymeleaf.cache: false关闭模板缓存,改完 HTML 立刻生效,省去反复重启;spring.datasource.url使用 H2 内存模式,jdbc:h2:mem:traveldb表示数据库名为traveldb,关闭时自动销毁,无残留。
2.3 第一个控制器:让首页http://localhost:8080/travel/返回景点列表
创建src/main/java/com/example/travel/controller/HomeController.java:
@Controller public class HomeController { @GetMapping("/") public String home(Model model) { // 模拟从数据库查出热门景点(实际应注入 JPA Repository) List<ScenicSpot> spots = Arrays.asList( new ScenicSpot("九寨沟", "四川阿坝", "世界自然遗产,以高山湖泊群著称", 5), new ScenicSpot("西湖", "浙江杭州", "人间天堂,苏堤春晓、断桥残雪", 5), new ScenicSpot("鼓浪屿", "福建厦门", "万国建筑博览,钢琴之岛", 4) ); model.addAttribute("spots", spots); return "index"; // 对应 templates/index.html } }对应src/main/resources/templates/index.html:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>旅游出行指南</title> </head> <body> <h1>热门景点推荐</h1> <div th:each="spot : ${spots}"> <h3 th:text="${spot.name}">景点名称</h3> <p>📍 <span th:text="${spot.location}">所在地</span></p> <p>📝 <span th:text="${spot.description}">简介</span></p> <p>⭐ <span th:text="${spot.rating} + '/5'">评分</span></p> <hr/> </div> </body> </html>启动应用后访问http://localhost:8080/travel/,页面将渲染出三个景点卡片。注意 Thymeleaf 的th:text会自动转义 HTML 字符,防止 XSS;若需插入富文本(如攻略中的<br>),改用th:utext。
3. 数据驱动:用 JPA 实体建模景点、城市与用户行为
3.1 三张核心表的设计逻辑与注解含义
旅游指南系统的核心数据模型围绕「地点」展开,需至少三张表:
scenic_spot(景点):存储名称、位置、描述、评分、所属城市 ID;city(城市):存储城市名、省份、经纬度、是否热门旅游地;user_favorite(用户收藏):记录用户 ID 与景点 ID 的多对多关系(此处简化为无用户登录的匿名收藏,用 IP 地址代替 user_id)。
对应 Java 实体类如下(使用 Lombok 简化 getter/setter):
@Entity @Table(name = "scenic_spot") @Data public class ScenicSpot { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(nullable = false) private String location; // 格式:"四川阿坝" @Column(columnDefinition = "TEXT") private String description; private Integer rating; // 1-5 分 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "city_id") private City city; @Column(name = "created_at", updatable = false) @CreationTimestamp private LocalDateTime createdAt; } @Entity @Table(name = "city") @Data public class City { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String name; // 如"杭州" private String province; // 如"浙江" private Double latitude; private Double longitude; @Column(name = "is_popular") private Boolean isPopular = false; } @Entity @Table(name = "user_favorite") @IdClass(UserFavoriteId.class) @Data public class UserFavorite { @Id private String ipAddress; // 匿名用户用 IP 标识 @Id private Long spotId; // 关联景点 ID @Column(name = "favorited_at") @CreationTimestamp private LocalDateTime favoritedAt; } // 复合主键类 @Data public class UserFavoriteId implements Serializable { private String ipAddress; private Long spotId; }关键注解说明:
@ManyToOne(fetch = FetchType.LAZY)表示查询景点时不立即加载城市信息,避免 N+1 查询问题;@CreationTimestamp由 Hibernate 自动填充创建时间,无需手动 set;@IdClass用于复合主键,比@EmbeddedId更直观,且UserFavoriteId必须实现Serializable;columnDefinition = "TEXT"显式指定 MySQL 的 TEXT 类型,避免 H2 默认的 VARCHAR(255) 截断长描述。
3.2 Repository 接口定义与 JPQL 查询实战
创建ScenicSpotRepository接口继承JpaRepository,即可获得findAll()、findById()等 20+ 方法:
@Repository public interface ScenicSpotRepository extends JpaRepository<ScenicSpot, Long> { // 按城市名模糊查询景点(JPQL) @Query("SELECT s FROM ScenicSpot s JOIN s.city c WHERE c.name LIKE %:cityName%") List<ScenicSpot> findByCityName(@Param("cityName") String cityName); // 按评分降序取前 10(原生 SQL,兼容性更强) @Query(value = "SELECT * FROM scenic_spot ORDER BY rating DESC LIMIT 10", nativeQuery = true) List<ScenicSpot> findTopRated(); // 按位置关键词搜索(H2 支持 FULLTEXT?不支持!改用 LIKE) List<ScenicSpot> findByNameContainingOrDescriptionContaining(String keyword, String keyword2); }在 Controller 中调用:
@Controller @RequestMapping("/spots") public class SpotController { private final ScenicSpotRepository spotRepository; public SpotController(ScenicSpotRepository spotRepository) { this.spotRepository = spotRepository; } @GetMapping("/top10") public String top10(Model model) { model.addAttribute("spots", spotRepository.findTopRated()); return "top10"; } @GetMapping("/search") public String search(@RequestParam String q, Model model) { // 注意:此处 q 直接传入,生产环境必须校验长度并过滤 SQL 关键字 model.addAttribute("spots", spotRepository.findByNameContainingOrDescriptionContaining(q, q)); model.addAttribute("keyword", q); return "search-result"; } }注意:
findByNameContainingOrDescriptionContaining自动生成的 SQL 是WHERE name LIKE ? OR description LIKE ?,两个参数值相同。若要支持多关键词(如“西湖 断桥”),需改用@Query手写 JPQL 并拆分字符串。
3.3 初始化测试数据:用data.sql在启动时预置 20 个景点
在src/main/resources/data.sql中插入初始数据(H2 启动时自动执行):
INSERT INTO city (name, province, latitude, longitude, is_popular) VALUES ('杭州', '浙江', 30.2641, 120.1551, true), ('成都', '四川', 30.5728, 103.7234, true), ('昆明', '云南', 25.0443, 102.7052, true); INSERT INTO scenic_spot (name, location, description, rating, city_id, created_at) VALUES ('西湖', '浙江杭州', '苏堤春晓、断桥残雪、雷峰夕照', 5, 1, NOW()), ('灵隐寺', '浙江杭州', '始建于东晋,江南著名古刹', 4, 1, NOW()), ('宽窄巷子', '四川成都', '清代古街道,融合老成都风貌', 4, 2, NOW()), ('春熙路', '四川成都', '成都最繁华商业街,网红打卡地', 3, 2, NOW()), ('滇池海埂大坝', '云南昆明', '冬季红嘴鸥聚集地,免费观鸥', 5, 3, NOW()); -- 此处省略其余 15 条...Spring Boot 默认启用spring.sql.init.mode=embedded,只要data.sql存在且数据库为空,就会在首次启动时执行。验证方法:启动后访问http://localhost:8080/h2-console(H2 控制台),输入JDBC URL: jdbc:h2:mem:traveldb,即可看到已建好的表和数据。
4. 页面交互升级:Thymeleaf 表单提交与收藏功能落地
4.1 构建景点详情页与收藏按钮的完整流程
详情页SpotDetailController需同时处理 GET 请求(展示)和 POST 请求(收藏):
@Controller @RequestMapping("/spots") public class SpotDetailController { private final ScenicSpotRepository spotRepository; private final UserFavoriteRepository favoriteRepository; public SpotDetailController(ScenicSpotRepository spotRepository, UserFavoriteRepository favoriteRepository) { this.spotRepository = spotRepository; this.favoriteRepository = favoriteRepository; } @GetMapping("/{id}") public String detail(@PathVariable Long id, Model model) { ScenicSpot spot = spotRepository.findById(id) .orElseThrow(() -> new RuntimeException("景点不存在")); model.addAttribute("spot", spot); // 查询该景点被收藏次数(简化版,实际应加 Redis 缓存) long count = favoriteRepository.countBySpotId(id); model.addAttribute("favoriteCount", count); return "spot-detail"; } @PostMapping("/{id}/favorite") public String favorite(@PathVariable Long id, HttpServletRequest request) { String ip = getClientIpAddress(request); // 防止重复收藏:检查该 IP 是否已收藏此景点 if (!favoriteRepository.existsById(new UserFavoriteId(ip, id))) { UserFavorite favorite = new UserFavorite(); favorite.setIpAddress(ip); favorite.setSpotId(id); favoriteRepository.save(favorite); } return "redirect:/spots/" + id; } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } }对应templates/spot-detail.html:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${spot.name} + ' - 旅游出行指南'">景点详情</title> </head> <body> <h1 th:text="${spot.name}">景点名称</h1> <p>📍 <span th:text="${spot.location}">所在地</span></p> <p>📝 <span th:utext="${spot.description}">简介</span></p> <p>⭐ <span th:text="${spot.rating} + '/5'">评分</span></p> <p>❤️ 已被 <span th:text="${favoriteCount}">0</span> 人收藏</p> <!-- 收藏表单 --> <form th:action="@{'/spots/' + ${spot.id} + '/favorite'}" method="post"> <button type="submit">收藏此景点</button> </form> <a href="/spots">返回列表</a> </body> </html>关键点说明:
th:utext用于渲染含 HTML 标签的description,如<br>换行;- 表单
th:action动态生成/spots/1/favorite路径,避免硬编码; getClientIpAddress()优先读取X-Forwarded-For头(反向代理场景),否则用getRemoteAddr();existsById()检查复合主键是否存在,比先查再 save 更高效。
4.2 静态资源与图片路径的正确配置方式
旅游网站必然需要景点图片。Spring Boot 默认将src/main/resources/static作为静态资源根目录,因此:
- 图片存放在
src/main/resources/static/images/scenic/下; - HTML 中引用为
<img th:src="@{/images/scenic/hangzhou-west-lake.jpg}" />; @{}是 Thymeleaf 的 URL 表达式,自动添加context-path(即/travel),最终解析为/travel/images/scenic/...。
若需上传图片,不应直接存到static目录(重启应用会丢失),而应:
- 创建
UploadService将文件保存到服务器磁盘(如/var/www/travel-images/); - 在
application.yml中配置spring.web.resources.static-locations=file:/var/www/travel-images/; - 前端通过
POST /api/upload接口上传,后端返回相对路径(如/uploads/20240701_abc.jpg)存入数据库。
提示:开发阶段可临时用
spring.resources.static-locations=classpath:/static/,file:./uploads/,将./uploads目录设为额外静态路径,便于快速测试。
4.3 城市筛选导航栏的动态生成逻辑
首页需显示「热门城市」标签栏,点击跳转到该城市景点列表。这需要从City表查出is_popular = true的城市,并生成带参数的链接:
@GetMapping("/") public String home(Model model) { List<City> popularCities = cityRepository.findByIsPopularTrue(); model.addAttribute("popularCities", popularCities); List<ScenicSpot> hotSpots = spotRepository.findTopRated(); model.addAttribute("spots", hotSpots); return "index"; }templates/index.html中添加导航:
<div class="city-nav"> <span>热门城市:</span> <th:block th:each="city : ${popularCities}"> <a th:href="@{/spots/city(name=${city.name})}" th:text="${city.name}">杭州</a> <span th:if="${#lists.indexOf(popularCities, city) < #lists.size(popularCities)-1}">|</span> </th:block> </div>对应新增 Controller 方法:
@GetMapping("/spots/city") public String spotsByCity(@RequestParam String name, Model model) { List<ScenicSpot> spots = spotRepository.findByCityName(name); model.addAttribute("spots", spots); model.addAttribute("cityName", name); return "city-spots"; }city-spots.html模板复用index.html的循环结构,仅标题改为<h1 th:text="${cityName} + '景点推荐'">杭州景点推荐</h1>。这样既避免重复代码,又保证 URL 可被搜索引擎收录(如/travel/spots/city?name=杭州)。
5. 生产就绪:打包部署、性能调优与常见报错排查
5.1 生成可执行 JAR 包并部署到 Linux 服务器
Maven 打包命令生成 fat jar(含所有依赖):
mvn clean package -DskipTests输出文件位于target/travel-guide-0.0.1-SNAPSHOT.jar。上传至服务器后,用以下命令后台运行:
nohup java -jar -Dspring.profiles.active=prod travel-guide-0.0.1-SNAPSHOT.jar > app.log 2>&1 &参数说明:
-Dspring.profiles.active=prod激活生产配置(需在src/main/resources/application-prod.yml中覆盖数据库连接为 MySQL);nohup使进程脱离终端;> app.log 2>&1将标准输出和错误重定向到日志文件;&后台运行。
验证服务是否启动:
curl -I http://localhost:8080/travel/ # 应返回 HTTP/1.1 200 OK若需开机自启,创建 systemd 服务文件/etc/systemd/system/travel-guide.service:
[Unit] Description=Travel Guide Spring Boot App After=network.target [Service] Type=simple User=deploy WorkingDirectory=/opt/travel ExecStart=/usr/bin/java -jar /opt/travel/travel-guide-0.0.1-SNAPSHOT.jar Restart=always RestartSec=10 [Install] WantedBy=multi-user.target然后执行:
sudo systemctl daemon-reload sudo systemctl enable travel-guide sudo systemctl start travel-guide5.2 三个高频报错的定位与修复方案
| 报错现象 | 日志关键词 | 根本原因 | 解决方案 |
|---|---|---|---|
Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter | ClassNotFoundException: javax.servlet.* | Spring Boot 3.x 迁移至 Jakarta EE 9,包名从javax.*变为jakarta.* | 检查所有依赖是否为 Spring Boot 3.x 兼容版本,特别是thymeleaf-extras-java8time需升级到4.0.0;删除web.xml和servlet-api依赖 |
Field userRepository in com.example.travel.controller.UserController required a bean of type 'com.example.travel.repository.UserRepository' that could not be found | required a bean of type ... that could not be found | Repository 接口未被 Spring 扫描到 | 确认@EnableJpaRepositories注解存在(通常@SpringBootApplication已包含),且接口包路径在主类同包或子包下;检查@Repository是否遗漏 |
There is already an object named 'scenic_spot' in the database | object named 'xxx' in the database | H2 数据库未清空,ddl-auto: create-drop失效 | 删除target/目录下h2临时文件;或改用ddl-auto: update并手动执行DROP ALL OBJECTS |
特别注意ddl-auto: update的风险:它只新增字段,不会删除废弃列,长期使用会导致表结构混乱。建议开发期用create-drop,上线前用 Flyway 或 Liquibase 管理迁移脚本。
5.3 JVM 参数调优与内存泄漏防护
默认 JVM 参数对旅游网站足够,但高并发时需调整:
- 添加
-Xms512m -Xmx1024m固定堆内存,避免频繁 GC; - 添加
-XX:+UseG1GC启用 G1 垃圾收集器,降低停顿时间; - 添加
-Dfile.encoding=UTF-8防止 Linux 系统默认编码导致中文乱码。
在application-prod.yml中开启 Actuator 端点监控内存:
management: endpoints: web: exposure: include: health,info,metrics,heapdump,threaddump endpoint: health: show-details: when_authorized访问http://your-server:8080/travel/actuator/heapdump可下载堆快照,用 VisualVM 分析内存占用。重点检查:
Thymeleaf模板是否因cache: true导致大量Template对象驻留;UserFavorite实体未加@Transactional时,save()调用可能引发 Session 泄漏;- 静态资源(如大图)未启用 Nginx 缓存,导致每次请求都走 Spring Boot。
最后,用jstat -gc <pid>实时观察 GC 情况,若FGCT(Full GC 时间)持续增长,说明存在内存泄漏,需结合 heapdump 定位对象引用链。
本文还有配套的精品资源,点击获取