☰
SpringBoot+Thymeleaf前后端分离旅游网站毕设实战解析
2026/10/2 3:51:49 网站建设 项目流程

简介:这套基于SpringBoot与Thymeleaf实现的旅游网站系统,采用前后端分离模式,适合Java方向毕业生作为毕业设计,也适合希望通过完整项目学习Web开发流程的初学者。压缩包共183个文件,包括44个Java后端核心类、28个HTML页面、18个CSS样式、16个JavaScript脚本,同时附带SQL数据库脚本与YML配置文件,整体大小仅9.57MB,目录组织清晰,便于定位源码、前端页面和数据库初始化内容。目前已有595人学习或下载。项目中引入Bootstrap等常见前端样式组件,jpg、png等图片素材数量充足,能帮助还原旅游站点的视觉效果;后端分层与Thymeleaf模板可相互对照,适合研究SpringBoot接口编写、数据持久化以及页面渲染的完整链路。这份压缩源码可直接用IntelliJ IDEA等工具导入运行,作为高分毕设参考或二次开发起点比较合适。

1. 旅游网站毕设里的“前后端分离”,和你想的可能不是一回事

先把这个标题拆开看:SpringBoot 负责后端接口和业务逻辑,Thymeleaf 负责服务端页面渲染,前后端分离负责把数据交互从页面渲染中剥离开——这三者放在同一个项目里,乍一听有点“既要又要”。但做过几个类似毕设项目后你会发现,这恰恰是本科毕设里最稳的组合:SpringBoot 撑起技术栈的“硬度”,Thymeleaf 让你不用在前端工程化上耗掉大量时间,而前后端分离的接口设计则让你的系统能被答辩老师一眼看出“有架构意识”。

这个标题适合谁?适合那些 Java 后端基础已经能跑通 SSM、但还不想碰 Vue/React 全家桶的同学。你要交付的不只是一个能跑的项目,而是一个“演示时经得起追问、查重时拿得出手、部署时不依赖 IDE”的系统。旅游网站这个业务域很讨巧:景点、线路、订单、评论、用户、收藏,既有 CRUD 也有业务关联,难度刚好卡在“不水”和“做得完”之间。

我在下面的内容里会按自己的实操习惯,把整个项目从选型理由、表结构、接口设计、模板渲染,一路讲到部署和答辩自测。你不需要照着抄每一行代码,但要理解每一步为什么这么定——毕设答辩问的就是这个。另外说句实在话,Thymeleaf 这个模板引擎做出的页面天然带有服务端渲染的味道,跟“前后端分离”这个说法存在天然的张力,所以第一章先把这一点讲透:这里的“前后端分离”,真正落地时是接口与视图分离、渲染与数据解耦,而不是把页面拆成独立的前端工程。

2. Thymeleaf + REST 双轨架构:毕设拿高分的稳妥选型怎么定

2.1 为什么是 Thymeleaf,而不是 JSP 或 Vue

先解决一个最容易在答辩时被老师问到的问题:你为什么要选 Thymeleaf,而不是 JSP,或者干脆上 Vue + Element UI 做彻底的前后端分离?

JSP 在 2024 年之后的 SpringBoot 项目里几乎算是“过时技术”,SpringBoot 官方对 JSP 的支持始终停留在“能用但别扭”的状态,打 war 包时要额外配置 JSP 编译器,内嵌 Tomcat 对 JSP 的 class 加载也有一堆兼容问题。更重要的是,JSP 在写复杂页面时容易把 Java 代码直接塞进视图层,答辩时被问到“你的控制层和视图层怎么解耦”会很难圆。

Vue 的问题是它把成本转嫁给了前端工程化:Node 环境、webpack/vite 配置、跨域处理、打包产物部署。本科毕设里,有相当一部分人的前端功底不足以在两周内搞定这整套链路,最后项目变成“能截图但跑不起来”,或者因为跨域配置出错导致接口一大半不通,演示现场翻车。

Thymeleaf 卡在两者中间:它不需要单独的前端构建工具链,直接放在src/main/resources/templates下,跟 Controller 天然配合;同时它的语法是纯 HTML 属性式扩展,没有 JSP 那种<% %>标签的割裂感。最关键的一点是,Thymeleaf 支持在开发时以“静态原型”方式直接打开 HTML 文件,这让你可以先做纯静态页面,再逐步用th:each、th:text等属性把数据填充进去——这对时间紧张的毕设来说,是一条很平滑的上手路径。

这时候再回头看“前后端分离”这个标题,我的落地建议是:接口层按前后端分离的标准来设计,返回 JSON;视图层用 Thymeleaf 做服务端渲染。也就是接口和视图在架构上解耦,但物理部署还在同一个 SpringBoot 应用里。这样做的好处是:如果答辩老师问“你能把前端换成 Vue 吗”,你可以直接说接口层已经全部按 RESTful 规范给出,前端只需要对接/api/**路径即可。

2.2 双轨架构下的表结构和模块划分

旅游网站系统的核心业务闭环是:用户浏览景点/线路 → 查看详情 → 下单预订 → 支付/模拟支付 → 生成订单 → 评论评分 → 收藏。围绕这个闭环,我在设计表结构时会把表分成三组,每组对应一个后端模块:

第一组是基础信息表:t_user(用户表)、t_scenic(景点表)、t_route(线路表)、t_hotel(酒店表)。景点和线路是本系统的核心资源,酒店可以看成附加资源,如果不做酒店模块,可以只保留前三张。

第二组是行为表:t_order(订单表)、t_comment(评论表)、t_favorite(收藏表)、t_cart(购物车表)。订单关联用户和线路,评论关联用户和景点/线路,收藏关联用户和资源。

第三组是辅助表:t_category(分类表)、t_banner(轮播图表)、t_notice(公告表)。

外键我建议在数据库中不要物理建立,逻辑上通过userId、scenicId这些字段关联即可。理由有两个:一是 MyBatis-Plus 或 MyBatis 的关联查询都是靠业务层手动拼接,物理外键反而容易在删数据时触发外键约束报错;二是毕设演示时如果配置了外键,删除一条已被引用的用户记录会直接抛异常,场面很难看。常见做法是依赖数据库唯一索引保证数据不重复,关联完整性靠代码层保证。

各表之间的关系是这样的:一个用户收藏多条线路,一张订单包含一条线路,一条线路挂在某个分类下,一个用户可以评论多条线路——这是一对多和多对多的经典组合。答辩时老师会看你的数据表能不能覆盖业务逻辑,能自圆其说就足够了,不需要像生产级系统一样建二十多张表。

2.3 后端包结构:答辩老师第一眼就检查的地方

包结构是毕设里最容易被老师打开看了的,同时也是最容易被扣分的点。一个只分controller、service、mapper三层,所有类都堆在三个包里的项目,哪怕功能全对,也顶多给个及格分。我习惯按模块分包,同时保留技术分层的二次划分:

com.example.travel ├── common // 通用类:Result封装、异常处理、常量 │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── PageResult.java ├── config // 配置类:WebMvcConfig、拦截器注册 ├── controller // 控制层:按业务模块细分 │ ├── user │ ├── scenic │ ├── route │ ├── order │ └── comment ├── service // 业务层:接口 + 实现 │ ├── impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端传入参数的接收对象 ├── vo // 返回给前端的数据对象 └── config // 分页插件、JSON配置

这里有一个很实用的经验:请求参数用 DTO 接收,返回数据用 VO 组装,数据库实体 Entity 不直接暴露给前端。比如新增订单时,前端传的是OrderAddDTO,包含userId、routeId、travelDate、peopleCount;后端处理后返回OrderVO,包含订单号、线路名称、总价、状态描述等。这样做的好处是,当实体类字段和前端需要的数据不一致时,你不会因为改 Entity 而导致数据库映射出错。

Controller 层的写法也要注意,每个方法尽量保持“参数接收 → 调用 Service → 返回 Result”的薄控制层模式,不要在 Controller 里写业务判断。我在项目中直接使用了 MyBatis-Plus 的多租户插件和分页插件,后者会在第四章专门讲参数配置。

3. 把旅游网站拆成可演示的模块:从数据表到接口再到页面

3.1 数据初始化脚本:用 SQL 预置演示数据

演示一个旅游网站,最尴尬的事情是页面上没有数据,旅游线路列表空荡荡。所以我会单独准备一份data.sql,预置十几条真实感的景点和线路数据,包括名称、价格、图片链接、描述、评分等字段。这份脚本在项目发布前执行到本地 MySQL 里,答辩演示时不用现场录入。

-- 预置分类数据 INSERT INTO t_category (id, name, sort, create_time) VALUES (1, '周边游', 1, NOW()), (2, '国内长线', 2, NOW()), (3, '出境游', 3, NOW()); -- 预置线路数据 INSERT INTO t_route (id, category_id, name, price, days, start_city, scenic_desc, image_url, status) VALUES (1, 2, '张家界国家森林公园4日深度游', 2680, 4, '北京', '天子山、袁家界、金鞭溪经典线路', '/images/route/zjj.jpg', 1), (2, 1, '杭州西湖+乌镇3日休闲游', 1880, 3, '上海', '西湖游船、乌镇西栅夜游', '/images/route/hz.jpg', 1), (3, 2, '成都+九寨沟6日纯玩团', 4280, 6, '广州', '熊猫基地、九寨沟全天游览、黄龙', '/images/route/jzg.jpg', 1);

图片链接这里是关键:如果你用网络图片地址,演示时一旦现场网络抖动,页面图片集体裂开。我把图片放在了src/main/resources/static/images/route/下,SQL 里直接写相对路径,由 SpringBoot 的静态资源映射对外暴露,整场演示零外部依赖。这个细节是我踩过坑之后才改的,第一次用外链图片做演示,现场连不上外部域名,整个页面看起来像没完成一样。

3.2 接口层:RESTful 风格 + 统一返回结果

前后端分离在接口层的体现非常直观:所有 AJAX 请求统一走/api/**,返回统一的 JSON 结构,然后由页面里的 jQuery 或原生 JS 接收处理后渲染。页面跳转用 Thymeleaf 的th:href生成 URL,数据刷新用$.get或$.post调接口获取。

统一返回结果类Result<T>的长这样:

@Data public class Result<T> { private Integer code; // 1 成功,0 业务失败 private String msg; // 提示信息 private T data; // 具体数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(1); r.setMsg("操作成功"); r.setData(data); return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.setCode(0); r.setMsg(msg); return r; } }

接口Controller的一个典型例子是景点列表分页查询:

@RestController @RequestMapping("/api/scenic") public class ScenicController { @Autowired private ScenicService scenicService; @GetMapping("/page") public Result<PageResult<ScenicVO>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "8") Integer pageSize, @RequestParam(required = false) String keyword) { PageResult<ScenicVO> result = scenicService.pageScenic(pageNum, pageSize, keyword); return Result.success(result); } }

说明一下这里的逻辑:PageResult<T>是一个自定义的通用分页返回体,包含total、pages、list三个字段,这样前端拿到数据后可以直接生成分页组件,不需要自己从 MyBatis-Plus 的IPage里再去逐个 get。keyword参数加required = false,允许搜索条件为空时做全量分页查询,这个设计在做搜索功能时很重要,避免了前端每次必须传一个空字符串。

3.3 Thymeleaf 页面:模板布局与列表页渲染

Thymeleaf 页面放在templates下,我用官方推荐的templates/layout方式抽公共头尾。先建一个layout.html放公共导航栏和页脚,子页面通过th:replace引用。

<!-- templates/layout/layout.html 公共导航片段 --> <nav class="navbar" th:fragment="nav"> <div class="container"> <a th:href="@{/}">首页</a> <a th:href="@{/scenic/list}">景点</a> <a th:href="@{/route/list}">线路</a> <div th:if="${session.loginUser != null}"> <span th:text="${session.loginUser.nickname}">用户昵称</span> <a th:href="@{/user/orders}">我的订单</a> </div> <div th:if="${session.loginUser == null}"> <a th:href="@{/login}">登录</a> <a th:href="@{/register}">注册</a> </div> </div> </nav>

列表页的渲染是 Thymeleaf 的核心用法,以线路列表route/list.html为例:

<div class="route-grid" th:each="route : ${page.records}"> <div class="route-card"> <img th:src="${route.imageUrl}" th:alt="${route.name}"> <h3 th:text="${route.name}">线路名称</h3> <p th:text="${route.days} + '天' + ${route.startCity} + '出发'">天数与出发城市</p> <span class="price" th:text="'¥' + ${route.price}">价格</span> <a th:href="@{/route/detail/{id}(id=${route.id})}">查看详情</a> </div> </div> <!-- 分页 --> <div class="pagination"> <a th:href="@{/route/list(pageNum=${page.current - 1})}" th:if="${page.current > 1}">上一页</a> <span th:text="${page.current} + ' / ' + ${page.pages}">页码</span> <a th:href="@{/route/list(pageNum=${page.current + 1})}" th:if="${page.current < page.pages}">下一页</a> </div>

这里的page对象是 Controller 里传给 Model 的PageResult,page.records对应当前页的数据列表,page.current是当前页码。需要注意th:each遍历的对象如果是空集合,页面什么都不会渲染,但不会报错,所以实际开发里我会在 Controller 里确保返回的PageResult不为 null,哪怕records为空列表。

3.4 前后端分离的边界在哪:什么时候用 Thymeleaf,什么时候走接口

这是整个项目里最容易被搞混的地方,我在这里给出一条非常明确的边界规则:

页面跳转和首屏数据,用 Thymeleaf 服务端渲染;页面内部的交互数据更新,走 Ajax 接口调 JSON。

举个例子:用户访问线路列表页/route/list,这个请求由 Controller 处理,查出分页线路数据,塞进 Model,Thymeleaf 渲染出完整 HTML 返回浏览器,首屏秒开。用户点击“收藏”按钮,此时页面不跳转,前端 JS 发起POST /api/favorite/add,后端返回Result<Void>,前端根据 code 弹出“收藏成功”提示。用户搜索线路时,如果搜索条件是整页刷新,可以直接把keyword参数拼到 URL 上重新请求;如果希望局部刷新,就调/api/route/page?keyword=xxx拿 JSON 再拼 HTML。

这套规则的最大好处是:答辩现场网络不好时,首屏页面依然能正常展示,因为它是服务端渲染出来的,不需要额外发请求;而交互功能用的是轻量 JSON,演示流畅度有保障。它同时也是对“前后端分离”这个标题最务实的解读——接口和视图解耦,但不需要拆成两个工程部署。

4. 让模板和接口数据对齐:Thymeleaf 参数传递、热更新与拦截器配置

4.1 SpringBoot 多环境配置:开发、演示、部署三份配置分离

这个项目我强烈建议从开始就做多环境配置,不要只留一个application.yml。答辩前你会频繁地在“本机开发”和“演示部署”之间切换,如果数据库连接串和端口每次都要手动改,很容易出现改了配置忘了改回、导致演示环境起不来的翻车事故。

# application-dev.yml 开发环境 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis-plus: configuration: 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
# application-prod.yml 演示/部署环境 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: travel_user password: Travel@2024 thymeleaf: cache: true

application.yml里只需要一行spring.profiles.active: dev指定默认环境。部署时用java -jar travel.jar --spring.profiles.active=prod切环境即可。注意thymeleaf.cache这个参数:开发时必须为false,否则你改完 HTML 要重启项目才能看到变化,非常拖节奏;部署时建议开true,减少模板解析开销。MyBatis-Plus 的StdOutImpl会在控制台打印每条 SQL,这对排查数据问题帮助极大,演示时如果嫌日志太吵,在 prod 环境关掉即可。

4.2 SpringBoot Thymeleaf 热更新配置:不用重启就能改页面

很多人不知道,SpringBoot 项目里 Thymeleaf 模板是支持热更新的,关键在于三件事:spring.thymeleaf.cache=false、spring.devtools.restart.enabled=true、以及 IDE 的自动编译开关。我一般会加 Spring Boot DevTools 依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>

DevTools 会在 classpath 内容变化时自动重启应用,对 Thymeleaf 模板来说,修改templates下的 HTML 文件后,只要 IDE 触发了编译输出到 target/classes,浏览器刷新就能看到效果,不需要手动重启。如果你是 IntelliJ IDEA,注意打开Settings → Build, Execution, Deployment → Compiler → Build project automatically,否则改了 HTML 不会自动拷贝到 target 里。

有个坑是:有些同学把templates放在src/main/resources下,改了文件但 target/classes 里没更新,页面一直不变,就以为热更新没用。检查方式是打开 target/classes/templates 目录看看 HTML 文件的最新修改时间,如果不是刚刚改完的时间,说明 IDE 没有自动编译,这时候手动Ctrl+Shift+F10或重新 Build 一次即可。

4.3 登录拦截器:页面级拦截与接口级拦截要分开做

旅游网站的订单、收藏、评论都要求登录后才能操作。我在项目里会用拦截器统一处理:页面请求未登录则跳转登录页,API 请求未登录则返回 JSON 错误码,由前端 JS 判断后弹窗提示,而不是粗暴地重定向。这个设计跟在前后端分离场景下的实际体验非常相关。

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); // 判断是否请求的是API接口 String uri = request.getRequestURI(); if (loginUser == null) { if (uri.startsWith("/api/")) { // API请求返回JSON,由前端处理跳转 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); } else { // 页面请求重定向到登录页 response.sendRedirect("/login"); } return false; } return true; } }

注册拦截器时要注意放行哪些路径:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/", "/login", "/register", "/logout", "/scenic/**", "/route/**", "/api/scenic/**", "/api/route/**", "/api/register", "/api/login", "/images/**", "/css/**", "/js/**", "/error" ); } }

excludePathPatterns里放的必须是全员可见的资源路径,比如景点和线路的浏览、静态图片和样式、登录注册接口。这里有一个我踩过的坑,会导致明明登录了却一直跳转登录页:用户在登录接口被放行了,登录成功后存了 session,但后续请求因为浏览器 Cookie 的 path 不对,导致 session 获取不到。排查方式是在浏览器开发者工具的 Network 面板看请求的Cookie头和Set-Cookie响应头,确认JSESSIONID有没有正常写入。

4.4 MyBatis-Plus 分页插件的必调参数

分页插件在 SpringBoot 里常用的是 MyBatis-Plus 的PaginationInnerInterceptor,配置代码如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(50L); // 单页最大条数,防止一次查全表 pagination.setOverflow(false); // 页码超出总数时是否回拨到第一页 interceptor.addInnerInterceptor(pagination); return interceptor; } }

setOverflow(false)这个参数值得解释一下:默认情况下,如果前端传的pageNum=999,MyBatis-Plus 会查出最后一页的数据而不是报错,这在演示时其实更安全,因为用户在分页组件里疯狂点下一页,不会出现白屏。但如果你希望超页时自动切回第一页,就把overflow打开。我习惯设成false返回最后一页数据,界面行为更自然。

分页插件的另一个注意点是:分页只对 Mapper 层的查询方法生效,如果你在 Service 层先list()全量查出再手动subList分页,插件完全帮不上忙,数据量一大页面直接卡死。正确写法是 Mapper 方法接收Page参数:

IPage<ScenicVO> selectScenicPage(Page<?> page, @Param("keyword") String keyword);

XML 文件里正常写SELECT语句,分页插件会在执行时自动拼接LIMIT,你在代码里不需要关心 SQL 的物理分页细节。

5. 毕设演示最容易翻车的 5 个坑:现象、原因、解决一条线

5.1 静态资源 404:图片、CSS 全挂,页面裸奔

现象:项目启动后页面能打开,但所有图片都裂图,CSS 样式完全没生效,整个页面像 1998 年的网站。

原因:Thymeleaf 模板里的资源路径写成了相对路径,比如src="images/logo.png",当前页面 URL 是/route/list,浏览器会把相对路径解析成/route/images/logo.png,而这个路径没有对应的静态资源映射,404。

解决:统一在模板里用 Thymeleaf 的 URL 表达式th:src="@{/images/logo.png}"。@{}会自动拼接项目的context-path,即使部署时改了应用名也不会错。同理,CSS 和 JS 引用都用th:href="@{/css/style.css}"。这个坑在页面刚写完、还没接数据时最容易出现,因为纯 HTML 原型用相对路径不会报错,一旦套上 Controller 的 URL 就暴露了。

5.2 部署到 Tomcat 后 session 频繁失效,登录状态保不住

现象:本地用java -jar跑得好好的,拿 war 包部署到外部 Tomcat 后,登录一会儿就掉线,或者每次重启 Tomcat 后所有用户都要重新登录。

原因:外部 Tomcat 的 session 默认是内存态,重启即丢失。更隐蔽的原因是,如果你的 SpringBoot 项目里同时用了spring-session-data-redis但 Redis 连接没配上,session 写入失败后静默降级,表现为“偶尔能登录、偶尔失效”。

解决:毕设项目建议直接用内部 Tomcat 跑 jar 包,不要打 war 包丢到外部 Tomcat。SpringBoot 的main方法启动的 Web 容器自带 session 管理,重启后重新登录是正常行为,答辩时只要演示流畅就行。如果你确实要部署外部 Tomcat,记得在application.yml里配置server.servlet.session.timeout: 30m并确保 Redis 配置正确,否则不要引入 spring-session。

5.3 前后端联调时接口返回 401,但手动在浏览器地址栏访问没问题

现象:Thymeleaf 页面能正常打开,但页面里用 JS 调/api/order/list时提示未登录,而直接在浏览器新标签页访问同一个接口却能拿到数据。

原因:拦截器放行了页面请求,但 JS 的$.get请求没有携带 Cookie,或者被拦截器判定为未登录。常见位置是:.ajax请求没设置xhrFields: { withCredentials: true },或者浏览器跨域拦截——如果你的页面是localhost:8080,接口是localhost:8081,这属于跨域,Cookie 默认不携带。

解决:同源部署的话,检查拦截器对/api/order/**是否错误地排除掉了。跨域的话,加一个全局 CORS 配置类,允许特定来源携带凭证:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:8080") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }

这个配置里的allowCredentials(true)是必须的,缺失时浏览器会拒绝携带 Cookie 的跨域请求。

5.4 Thymeleaf 页面里的日期格式变成一串数字

现象:列表页显示线路出发日期,页面上出现1722048000000这样的一串数字,而不是2024-07-27。

原因:Java 后端直接往 Model 里塞了Date或LocalDateTime对象,Thymeleaf 默认调用toString(),而LocalDateTime.toString()输出的是 ISO 格式,不是你想的yyyy-MM-dd。

解决:在 Entity 或 VO 的日期字段上加@JsonFormat只能解决 JSON 序列化,对 Thymeleaf 模板渲染没用。模板里要用#temporals工具类格式化:

<span th:text="${#temporals.format(route.departureDate, 'yyyy-MM-dd')}">出发日期</span>

如果是Date类型,用#dates.format(route.departureDate, 'yyyy-MM-dd')。建议在 VO 层直接先把日期转成格式化好的字符串,Controller 传到 Model 的数据就已经是展示格式,模板里不需要做任何处理。这个做法可以避免 5 个页面上出现 5 种日期格式不一致的问题。

5.5 上传图片后刷新页面图片就没了,文件存在哪是个问题

现象:后台管理的轮播图功能,上传一张图片后,当时能显示,但重启项目或重新部署后图片丢失。

原因:图片被写到了项目的运行时目录,比如src/main/resources/static/upload/,在 IDE 里跑的时候写的是 target/classes 下的临时文件,重启后 target 被清理,文件就没了。idea里直接运行和java -jar运行的文件路径还不一样,极其容易踩。

解决:把上传根目录配置到系统固定路径,比如/usr/local/travel/upload/,然后通过自定义静态资源映射对外暴露:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = System.getProperty("user.dir") + File.separator + "upload" + File.separator; registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); }

注意addResourceLocations必须写成file:开头,否则不会以文件系统方式读取。这个改动虽然小,但能让你的上传功能在演示后重启依然保留数据,属于“答辩后悔药”级别的配置。

6. 答辩前一周的自测清单:从数据到部署的收尾技巧

最后这部分,给你一套我在交付毕设前会完整走一遍的自测流程,按顺序做,能筛掉 90% 演示现场会出问题的隐患。

第一步是清库重置。在演示前一天的晚上,把数据库里的测试数据删掉,重新执行data.sql和建表脚本,得到一个全量干净的数据环境。这样做是因为开发过程中你可能会乱改数据、删了某条关键记录,导致某个页面显示异常。干净数据加上你亲手验证过的联调路径,是整个演示流程的地基。顺序是:drop database travel_db→create database travel_db→ 执行schema.sql→ 执行data.sql。

第二步是用生产环境配置跑一遍整流程。切到 prod 配置文件,别用 dev 环境做最终验收。dev 环境开的 SQL 日志、模板热更新、DevTools 自动重启,在演示时可能因为控制台刷屏干扰注意力,或者因为模板缓存的差异导致页面行为和开发时不一样。执行java -jar travel-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod,然后用浏览器走一遍:注册 → 登录 → 浏览列表 → 搜索 → 查看详情 → 收藏 → 下单 → 查订单 → 评论。任何一步卡顿,都要在当时记下来。

第三步是检查所有外链资源。打开页面前端 30 秒内,把页面里所有图片、JS、CSS 加载情况过一遍 Network 面板,确认没有红色的失败请求。特别注意那些在开发时用网络图片 URL 的位置,如果演示现场网络条件差,提前全部替换成本地资源。这一步只要做一次,就能避免整场演示变成满屏裂图的灾难现场。

第四步是准备一份前后端接口对应关系表。不需要交到老师手里,是给你自己答辩准备的。老师问“你这里前后端怎么交互的”,你能直接说出:/api/scenic/page对应景点列表页的搜索分页,Thymeleaf 渲染首屏,JS 负责收藏操作;/api/order/create对应订单提交,数据库事务保证库存和订单的一致。这张表的价值是让你的项目图景在答辩时清晰到可以随时被追问。下面是一个简化的对接表样式:

页面路由首屏渲染方式交互接口返回数据
/route/listThymeleaf 服务端渲染/api/route/pagePageResult<RouteVO>
/route/detail/1Thymeleaf 服务端渲染/api/favorite/checkResult<Boolean>
/user/ordersThymeleaf 服务端渲染/api/order/listResult<List<OrderVO>>

第五步是录一段演示视频。这是我自己的习惯:提前用屏幕录制工具跑一遍核心流程,留作备份。万一现场电脑投屏出问题,或者数据库连不上,你至少可以播放视频完成演示,不至于站在那里尴尬。这也是给自己的一颗定心丸,实际演示时反而更松弛,因为最坏情况已经有兜底方案了。

末了说句心里话,这类旅游网站系统,技术上没有哪个点是真的“高深”,它的价值在于把 CRUD、认证、模板渲染、接口设计这些基本功拼装成一个完整闭环,并且每一步都有据可查。答辩时老师最看重的就是你能不能讲清楚“为什么这么设计”,而不是代码本身多炫。我的教训是:代码写得再漂亮,不如把演示环境多跑三遍来得踏实。希望帮到你,祝答辩顺利。

本文还有配套的精品资源,点击获取

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

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

立即咨询