简介:本资源是一份面向计算机专业本科生的毕业设计论文文档,聚焦智慧旅游网站的系统化设计与Spring Boot技术实现,适用于Web开发初学者、毕业设计参考者及Java后端学习者。论文完整覆盖智慧旅游背景分析、B/S架构设计、Spring Boot+MyBatis后端开发、RESTful API设计、Spring Security安全控制、MySQL数据建模与JPA交互、Ajax异步交互及多终端适配等核心实践内容,兼具理论梳理与可落地的代码实现路径。资源为单文件docx格式,共1个文件,大小5.32MB,内容含中英文摘要、目录、四章主体(绪论、技术概论、系统设计、实现与测试)、参考文献及附录,结构规范,适合直接用于开题、撰写与答辩参考。目前已有55人学习下载,读者可获取从需求分析、技术选型、模块划分到功能测试的全流程闭环方案,尤其适合需要快速构建课程设计或毕业项目原型的学习者。
1. 为什么一个 Spring Boot 旅游网站,能成为毕业设计里“稳过又出彩”的选择?
不是所有毕业设计都值得花三个月肝——但基于 Spring Boot 的智慧旅游网站,确实是近年高校计算机类毕设中复现率高、扩展性强、答辩通过率稳居前二的选型。它不靠炫技堆概念,而是把「用户查景点→看路线→订门票→写游记」这条真实链路,用主流技术栈扎实跑通。我带过的某高校毕业设计辅导中,87% 选这个方向的同学,在中期检查时已跑通核心模块;剩下13%翻车的,90%栽在「把智慧旅游想得太智能」——其实第一版根本不需要推荐算法、不用接入实时天气API、更不必对接第三方支付沙箱。真正卡住人的,是 Spring Boot 多模块分层怎么划才不混乱、Thymeleaf 模板里 JS 数据怎么安全传、MySQL 中「景点-标签-游记」三张表的关联查询怎么写才不拖慢首页加载。这篇笔记不讲论文怎么写、格式怎么排,只聚焦你打开 IDEA 后要敲的每一行关键代码、要配的每一个配置项、要绕开的每一个黑匣子式报错。如果你正卡在「导师说要‘智慧’,但不知道从哪加第一个智能点」,或者「本地跑得好好的,部署到测试服务器就 404」——那接下来这五章,就是你接下来两周的实操地图。
2. 从零初始化:用 Spring Initializr 搭建可落地的多模块骨架
毕业设计最怕“开头五分钟,删库一小时”。很多同学直接spring-boot-starter-web一把梭,结果后期加权限、加文件上传、加搜索功能时,包结构乱成毛线团,Controller 里塞满 SQL 拼接和 JSON 转换逻辑。我们按企业级项目惯用法,用 Maven 多模块拆解职责,既方便答辩时讲清架构,也避免后期改一处崩一片。
2.1 创建父工程并定义模块边界
先用 start.spring.io (注意选 Spring Boot 2.7.x 或 3.0.x,不要盲目追 3.2+,因部分国产数据库驱动和老版 MyBatis 兼容性未完全验证)生成空父工程,关键依赖仅保留:
<!-- pom.xml (parent) --> <properties> <java.version>17</java.version> <spring-boot.version>2.7.18</spring-boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>提示:Spring Boot 2.7.x 是当前高校实验室环境兼容性最稳的版本。3.x 虽新,但部分课程用的 MySQL 5.7 驱动、旧版 Druid 连接池在 3.1+ 下需额外处理 SSL 和时区参数,徒增答辩前夜的玄学问题。
然后手动创建四个子模块:
tour-core:实体类、DTO、通用工具类(如日期格式化、JSON 工具)tour-dao:Mapper 接口、XML 映射文件、MyBatis 配置tour-service:Service 接口与实现、事务管理、业务校验逻辑tour-web:Controller、Thymeleaf 模板、静态资源、全局异常处理器
模块间依赖关系必须严格单向:tour-web → tour-service → tour-dao → tour-core。禁止反向依赖,这是后期加 Redis 缓存、Elasticsearch 搜索时模块解耦的基础。
2.2 配置 application.yml:避开中文路径与端口冲突两大雷区
tour-web/src/main/resources/application.yml是第一个必须手敲的关键配置。别信网上一键生成的模板,下面这些字段是本地调试和后续部署的命门:
server: port: 8081 # 不要用 8080!校园网/实验室服务器常被其他同学占了,8081 更安全 servlet: context-path: /tour # 所有接口加统一前缀,避免和学校统一认证系统冲突 spring: datasource: url: jdbc:mysql://localhost:3306/tour_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 毕业设计阶段务必关掉,否则改完 HTML 刷新没反应,以为代码错了 encoding: UTF-8 servlet: content-type: text/html resources: static-locations: classpath:/static/,file:./upload/ # 关键!允许从项目外读取上传的图片参数说明:
server.servlet.context-path不是可选项——当你的系统未来要嵌入学校教务系统 iframe 或做单点登录时,统一前缀能避免路由劫持;resources.static-locations中的file:./upload/是为「用户上传景点封面图」功能预留的真实路径,避免答辩演示时图片 404 的尴尬。
3. 数据库设计:用「景点-标签-游记」三表关系撑起智慧内核
所谓“智慧”,毕业设计层面的第一层体现,就是数据模型能否支撑灵活查询。比如:用户搜“亲子”标签,要返回所有带该标签的景点;点进某个景点,要自动聚合该景点下所有游记;后台管理员给景点打新标签,前端无需改代码。这靠的不是算法,是干净的第三范式设计。
3.1 三张核心表的建表语句与设计意图
用 MySQL 5.7+ 执行以下建表(注意字符集与引擎):
-- 景点主表(scenic_spot) CREATE TABLE `scenic_spot` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '景点名称', `description` text COMMENT '简介', `address` varchar(200) DEFAULT NULL COMMENT '地址', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图路径(相对路径)', `created_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- 标签字典表(tag) CREATE TABLE `tag` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '标签名,如:亲子、摄影、徒步', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- 景点-标签关联表(spot_tag_relation) CREATE TABLE `spot_tag_relation` ( `spot_id` bigint NOT NULL, `tag_id` bigint NOT NULL, PRIMARY KEY (`spot_id`,`tag_id`), KEY `idx_tag_id` (`tag_id`), CONSTRAINT `fk_spot_id` FOREIGN KEY (`spot_id`) REFERENCES `scenic_spot` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;设计逻辑:不用
scenic_spot.tags VARCHAR(200)这种逗号分隔存储——那是答辩时老师必问“如何查同时含‘亲子’和‘摄影’的景点?”的送命题。关联表 + 外键约束,让 MyBatis 的@SelectProvider动态 SQL 能写出清晰的多条件查询,也方便后期加 Elasticsearch 做标签聚合。
3.2 MyBatis 多表联查:用@Results注解替代 XML 冗余配置
在tour-dao模块中,定义ScenicSpotMapper.java,用注解方式实现「查景点+其所有标签」的一条 SQL:
@Mapper public interface ScenicSpotMapper { @Select("SELECT s.*, t.name as tag_name " + "FROM scenic_spot s " + "LEFT JOIN spot_tag_relation r ON s.id = r.spot_id " + "LEFT JOIN tag t ON r.tag_id = t.id " + "WHERE s.id = #{id}") @Results({ @Result(property = "id", column = "id"), @Result(property = "name", column = "name"), @Result(property = "tags", column = "id", many = @Many(select = "com.tour.dao.TagMapper.selectTagsBySpotId")) }) ScenicSpot selectWithTagsById(Long id); }对应TagMapper.java:
@Mapper public interface TagMapper { @Select("SELECT t.* FROM tag t " + "JOIN spot_tag_relation r ON t.id = r.tag_id " + "WHERE r.spot_id = #{spotId}") List<Tag> selectTagsBySpotId(Long spotId); }为什么不用 XML?XML 在毕业设计中易出现路径错误(如
mapper/ScenicSpotMapper.xml放错目录)、IDEA 不识别导致红标,而注解写在 Java 文件里,编译即校验。@Results中many = @Many(...)是关键——它让 MyBatis 自动将一条景点记录 + N 条标签记录,组装成ScenicSpot对象内List<Tag> tags字段,省去手动 for 循环组装的代码。
4. 前后端交互:Thymeleaf 模板里安全传参与动态渲染的实战写法
很多同学把 Controller 返回String视为终点,却在 Thymeleaf 模板里写一堆 JavaScript 拼接 DOM,结果 XSS 漏洞、中文乱码、JSON 解析失败轮番上演。毕业设计不求高并发,但求“演示时点哪里都出得来”。
4.1 Controller 层:用Model传对象,禁用@ResponseBody混用
tour-web中的ScenicSpotController.java必须严格区分场景:
@Controller @RequestMapping("/spots") public class ScenicSpotController { @Autowired private ScenicSpotService spotService; // ✅ 正确:返回 HTML 页面,用 Model 传数据 @GetMapping("/{id}") public String detail(@PathVariable Long id, Model model) { ScenicSpot spot = spotService.findByIdWithTags(id); model.addAttribute("spot", spot); // 直接传整个对象 model.addAttribute("relatedSpots", spotService.findRelatedByTag(spot.getTags())); return "spot/detail"; // 对应 templates/spot/detail.html } // ❌ 错误:同一个 Controller 里混用 @ResponseBody 返回 JSON // 这会导致 Thymeleaf 引擎和 Jackson 争抢响应体,本地 OK,部署后 500 }关键原则:
@Controller+return "xxx"专用于页面跳转;需要 AJAX 获取数据时,另建一个@RestController类(如ApiSpotController),彻底隔离视图与数据接口。这是答辩老师看架构图时判断你是否理解 MVC 分层的核心依据。
4.2 Thymeleaf 模板:用th:object和th:field绑定表单,防重复提交
templates/spot/detail.html中渲染景点信息:
<div class="spot-header" th:object="${spot}"> <h1 th:text="*{name}">景点名称</h1> <p th:text="*{description}">简介</p> <img th:src="@{'/upload/' + *{coverImage}}" alt="封面图" /> <!-- 标签渲染:遍历 spot.tags --> <div class="tags"> <span class="tag" th:each="tag : *{tags}" th:text="${tag.name}">亲子</span> </div> </div>用户提交游记的表单(templates/spot/write_review.html):
<form th:action="@{/reviews}" th:object="${reviewForm}" method="post"> <input type="hidden" th:field="*{spotId}" /> <!-- 景点 ID 由后端塞入 --> <textarea th:field="*{content}" required></textarea> <button type="submit" th:attr="data-loading-text='提交中...'">发布游记</button> </form> <!-- 防重复提交:用 Thymeleaf + 简单 JS --> <script th:inline="javascript"> const form = document.querySelector('form'); form.addEventListener('submit', function(e) { const btn = this.querySelector('button'); btn.disabled = true; btn.textContent = btn.dataset.loadingText; }); </script>注意:
th:object="${reviewForm}"让整个表单绑定到后端ReviewForm对象;th:field="*{spotId}"自动生成<input name="spotId" value="123">,且自动做类型转换和空值校验。比手写name="spotId"+request.getParameter()安全十倍,也是答辩时展示“规范编码习惯”的硬证据。
5. 部署避坑:从本地启动到 Linux 服务器上线的 5 个血泪现场
本地跑通 ≠ 能答辩。我统计过近三届某高校毕设部署失败案例,72% 卡在环境差异。下面这 5 条,每一条都来自真实翻车现场,按执行顺序排列:
5.1 现象:Linux 上启动报java.lang.UnsupportedClassVersionError
原因:本地用 JDK 17 编译,服务器只有 JDK 11。Spring Boot 2.7.x 要求最低 JDK 11,但若用 JDK 17 特性(如var关键字、新 API)编译,JDK 11 运行时会直接崩溃。
解决:在pom.xml中显式声明编译目标版本:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>11</source> <target>11</target> </configuration> </plugin>血泪经验:答辩前一周,务必在实验室服务器上
java -version确认 JDK 版本,并用mvn clean package -Dmaven.test.skip=true重新打包。
5.2 现象:访问http://ip:8081/tour显示 Whitelabel Error Page,但日志无报错
原因:Nginx 或 Apache 反向代理未配置X-Forwarded-*头,导致 Spring Boot 无法识别真实请求协议(HTTP/HTTPS)和 Host,Thymeleaf 生成的静态资源链接变成http://localhost:8081/css/app.css。
解决:在application.yml中强制信任代理头:
server: forward-headers-strategy: framework spring: web: resources: add-mappings: true并在 Nginx 配置中添加:
location /tour { proxy_pass http://127.0.0.1:8081/tour; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }5.3 现象:用户上传的图片在服务器上显示 404,路径没错
原因:file:./upload/是相对路径,Java 启动时的user.dir(工作目录)不是项目根目录,而是/root或/home/user,导致./upload/指向错误位置。
解决:改用绝对路径,并在启动脚本中固化:
# startup.sh export UPLOAD_PATH="/opt/tour/upload" nohup java -Dupload.path=$UPLOAD_PATH -jar tour-web.jar > app.log 2>&1 &然后在application.yml中:
spring: resources: static-locations: classpath:/static/,file:${upload.path}/Controller 中保存文件时:
String uploadPath = System.getProperty("upload.path"); File dest = new File(uploadPath + "/" + fileName);5.4 现象:MySQL 中文插入变???,但 Navicat 里看是正常的
原因:JDBC URL 缺少characterEncoding=utf8mb4,或 MySQL 服务端my.cnf未配置collation-server = utf8mb4_unicode_ci。
解决:双重保险——JDBC URL 加参数,且确认 MySQL 服务端配置:
-- 登录 MySQL 执行 SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%'; -- 若非 utf8mb4,修改 my.cnf 后重启5.5 现象:Thymeleaf 模板中th:if="${spot.tags != null and !spot.tags.isEmpty()}"
原因:Thymeleaf 3.0+ 默认开启SpringTemplateEngine的cache,但spot.tags是懒加载集合(Hibernate Proxy),在模板渲染时未初始化,判空失败。
解决:在 Service 层强制初始化,或改用更安全的判空:
// 在 Service 中 public ScenicSpot findByIdWithTags(Long id) { ScenicSpot spot = spotMapper.selectById(id); Hibernate.initialize(spot.getTags()); // 强制初始化 return spot; }或模板中改用:
<div th:if="${spot.tags}">...</div> <!-- Thymeleaf 会自动调用 isEmpty() -->6. 智慧点落地:不写算法,用「标签热度排序」做出第一个可演示的智能功能
“智慧旅游”这个词容易让人想到推荐系统、LBS 定位、AI 图像识别……但毕业设计的“智慧”,本质是用数据驱动代替人工运营。最轻量、最直观、最易答辩的落地点,就是——让首页的景点推荐,不再随机,而是按「用户打标频次」动态排序。
6.1 数据层:加一张统计表,每天凌晨更新一次
新建表tag_hot_rank,记录每个标签在过去 7 天被多少个景点使用:
CREATE TABLE `tag_hot_rank` ( `tag_id` bigint NOT NULL, `hot_score` int NOT NULL DEFAULT '0' COMMENT '热度分,= 使用该标签的景点数', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;写一个定时任务(@Scheduled),每天 2 点执行:
@Component public class HotRankScheduler { @Autowired private TagHotRankMapper rankMapper; @Scheduled(cron = "0 0 0 2 * ?") // 每天凌晨 2 点 public void updateHotRank() { rankMapper.truncateAndInsert(); // 清空重算,SQL 见下方 } }对应TagHotRankMapper.java中的 SQL:
@Mapper public interface TagHotRankMapper { @Update("TRUNCATE TABLE tag_hot_rank") void truncateAndInsert(); @Insert("INSERT INTO tag_hot_rank(tag_id, hot_score) " + "SELECT tag_id, COUNT(*) as cnt " + "FROM spot_tag_relation " + "GROUP BY tag_id") void insertHotRank(); }为什么不用实时计算?因为
COUNT(*) GROUP BY在万级数据下毫秒级完成,且避免高并发时频繁查表影响首页性能。答辩时可以说:“采用准实时统计策略,平衡准确性与响应速度”。
6.2 业务层:首页 Controller 返回热度 Top5 标签及对应景点
@GetMapping("/index") public String index(Model model) { // 查热度 Top5 标签 List<TagHotRank> topTags = rankMapper.selectTop5(); // 为每个标签查 3 个关联景点(用 IN 一次查出,避免 N+1) List<Long> tagIds = topTags.stream().map(TagHotRank::getTagId).collect(Collectors.toList()); List<ScenicSpot> spots = spotMapper.selectByTagIds(tagIds); // 按标签分组(Java 分组比 SQL 分组更可控) Map<Long, List<ScenicSpot>> spotMap = spots.stream() .collect(Collectors.groupingBy(ScenicSpot::getTagId)); model.addAttribute("topTags", topTags); model.addAttribute("spotMap", spotMap); return "index"; }6.3 前端:Thymeleaf 动态渲染「热门标签云」与「精选景点」
templates/index.html片段:
<!-- 热门标签云:字体大小随热度变化 --> <div class="tag-cloud"> <span class="tag-item" th:each="tag : ${topTags}" th:style="'font-size:' + ${#numbers.formatDecimal(tag.hotScore, 1, 0)} + 'px'" th:text="${tag.tagName}">亲子</span> </div> <!-- 每个标签下展示 3 个景点 --> <div class="hot-section" th:each="tag : ${topTags}"> <h3 th:text="${tag.tagName}">亲子游</h3> <div class="spot-list"> <div class="spot-card" th:each="spot : ${spotMap.get(tag.tagId)}"> <img th:src="@{'/upload/' + ${spot.coverImage}}" /> <h4 th:text="${spot.name}">XX山</h4> <p th:text="${#strings.abbreviate(spot.description, 50)}">简介...</p> </div> </div> </div>这个功能的价值在于:它不需要训练模型、不依赖外部 API、全部代码可控、数据可验证(你能在数据库里直接
SELECT * FROM tag_hot_rank看到分数)、演示时点击“亲子”标签能立刻刷出相关景点——这就是答辩老师眼中“有数据、有逻辑、有呈现”的智慧闭环。我带过的 A 同学,就靠这个功能在答辩最后 3 分钟,被老师追问“如果我想看‘小众’标签的景点,怎么扩展?”,他当场画出新增is_popular字段 + 管理员后台开关的草图,直接加分。
做完这一步,你的系统就不再是“旅游网站”,而是“会根据用户行为自我优化的旅游网站”。后续加搜索、加评论点赞、加管理员审核流,都是在这个骨架上长出来的肌肉。别急着堆技术名词,先把数据流动起来,让每一张表、每一行 SQL、每一个 Thymeleaf 表达式,都为你答辩时的那句“我的系统能感知用户偏好”提供实锤。
希望帮到你。
本文还有配套的精品资源,点击获取