
简介面向Java毕业设计学生的SpringBootThymeleaf旅游网站系统源码采用前后端分离设计整体涵盖旅游线路展示、景点介绍、用户登录注册以及后台管理等核心模块适合用于课程设计或毕业设计二次开发。压缩包共183个文件大小9.57MB以44个java源文件为后端业务核心配合28个html页面、18个css样式与16个js脚本实现前端展示和交互37个jpg、10个png以及gif/svg等图片素材用于丰富页面细节另附sql数据库脚本、yml配置以及project、prefs、iml等IDE配置可直接导入数据库后用IDEA或Eclipse打开运行目录结构清晰便于按模块理解SpringBoot与Thymeleaf的整合方式。该资源已有595人学习下载对希望系统掌握旅游类网站开发流程、熟悉前后端数据交互的读者来说是一份完整且具备很强参考价值的毕业设计源码。1. 为什么这套“伪分离”架构才是高分毕设的正确打开方式如果你把“SpringBootThymeleaf前后端分离”这几个词放在一起交给稍微懂行的人看第一反应一定是“这俩不是一回事”。Thymeleaf 是服务端渲染模板前后端分离通常是 SpringBoot 提供 JSON 接口、Vue 或 React 单独部署。但毕设评分的重点从来不是架构纯度而是业务完整度、代码规范、答辩时你能不能自圆其说。这套方案真正落地时是“半分离”首屏页面由 Thymeleaf 服务端渲染动态数据和交互走 SpringBoot 的 JSON 接口前端只用少量 JS 在页面里消费这些接口。这种方式能在一到两周内做出一个旅游网站该有的全部业务闭环还能把「接口设计」「拦截器」「事务」「分页」这些加分点全塞进去。适合两类人时间紧、想稳妥过审拿高分的应届生以及想用最低集成成本把旅游业务做完整的后端初学者。2. SpringBoot 项目骨架先把依赖和分层定死后面才不返工2.1 依赖选择与 pom.xml 里最容易抄错的两个版本旅游网站这种 CRUD 为主的系统技术栈越稳越好。我一般选 SpringBoot 2.7.x 而不是 3.x原因很简单3.x 对 javax 改 jakarta很多毕设参考代码和网上的 MyBatis 整合文章直接报错答辩前换 API 是最难受的。2.7.x 在这个时间点依然是毕设和中小项目里最稳的选择资料多、报错一搜就有答案。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId optionaltrue/optional /dependency /dependencies两个容易抄错的地方。第一个是mysql-connector-java在 2.7.x 里不需要写版本号parent 帮你管好了如果你换到 3.x这里要改成com.mysql:mysql-connector-j。第二个是 PageHelper 的 starter 版本不要超过 1.4.7跟 SpringBoot 2.7 配合最稳定版本再高容易出现分页拦截器不生效的玄学问题。2.2 分层目录与 application.yml 参数把热更新和路径一次配好目录结构直接按 controller / service / mapper / entity / config / interceptor / common 分。这不是随便分的——答辩老师翻你项目时第一眼看的不是功能是包结构是否规整。我见过太多同学把所有类都堆在 controller 里接口一多自己都找不着这种项目功能再全也难拿高分。server: port: 8080 servlet: context-path: /travel spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encoding: UTF-8 web: resources: static-locations: classpath:/static/ mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagehelper: helper-dialect: mysql reasonable: truecontext-path: /travel是毕设里最容易漏的配置。如果不设置你部署到 Tomcat 时访问路径是localhost:8080/项目名/本地启动又是localhost:8080/页面里所有链接和接口路径都得跟着改非常痛苦。设置成固定前缀后本地和服务器体验一致。cache: false配合 devtools是 SpringBoot Thymeleaf 热更新的核心开关后面避坑章节会细讲。静态资源目录默认就是classpath:/static/你只需要把 CSS、JS、图片放进 resources/static 下对应文件夹。注意static-locations这个配置不要随便改成别的路径改完以后th:src{/js/app.js}这类模板 url 表达式会找不着资源现场翻车概率极高。3. 前后端“半分离”的实现JSON 给数据、Thymeleaf 给骨架3.1 统一返回体与第一个 JSON 接口让老师一眼看到“分离”前后端分离的核心标志是接口返回结构化数据而不是拼接好的 HTML 片段。所以第一步是定义一个统一的ResultT所有 controller 都返回它。这个类要写成一个泛型类包含 code、message、data 三个字段并提供 success 和 error 两个静态工厂方法。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message 操作成功; r.data data; return r; } public static T ResultT error(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } // getter / setter 省略 }RestController RequestMapping(/api/line) public class LineController { Resource private LineService lineService; GetMapping(/list) public ResultPageInfoLine list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 8) int pageSize) { PageHelper.startPage(pageNum, pageSize); ListLine lines lineService.findAll(); return Result.success(new PageInfo(lines)); } }逻辑说明用RestController而不是Controller这样方法直接返回 JSON不会走 Thymeleaf 的视图解析器。分页用 PageHelper 的静态方法startPage它会在下一句查询语句执行时自动拼接 limit查询完再用PageInfo包装拿到 total、pages、pageNum 这些前端分页组件需要的数据。参数 pageNum 和 pageSize 都给了默认值前端不传也不会报错。这个Result类是整个项目的门面。它存在一个隐藏加分点全局异常处理器RestControllerAdvice里统一返回Result.error(e.getMessage())这样前端只需要判断code 200其他情况弹错误提示。这种约定式错误处理在答辩时比每一个接口单独 try-catch 高级得多。3.2 Thymeleaf 页面消费 JSON列表页的完整链路页面这一侧我的做法是列表页的 HTML 骨架和首屏数据用 Thymeleaf 模板渲染翻页、搜索、下单这类交互动作全部走 Ajax 调上面的 JSON 接口拿到数据后用 JS 拼 DOM 或者重新渲染。table classtable thead trth线路名称/thth价格/thth操作/th/tr /thead tbody idlineBody !-- 首屏数据由 Thymeleaf 渲染 -- tr th:eachline : ${lines} td th:text${line.name}/td td th:text¥ ${line.price}/td tdbutton classbtn-book th:attrdata-id${line.id}预订/button/td /tr /tbody /table// 页面底部引入 axios调用分页接口 function loadLines(pageNum) { axios.get(/travel/api/line/list, { params: { pageNum: pageNum, pageSize: 8 } }).then(function (resp) { if (resp.data.code ! 200) { alert(resp.data.message); return; } var list resp.data.data.list; var html ; list.forEach(function (line) { html trtd line.name /tdtd¥ line.price /tdtdbutton classbtn-book>CREATE TABLE user ( id bigint PRIMARY KEY AUTO_INCREMENT, username varchar(50) NOT NULL UNIQUE, password varchar(100) NOT NULL, phone varchar(20), create_time datetime DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tour_line ( id bigint PRIMARY KEY AUTO_INCREMENT, name varchar(100) NOT NULL, city varchar(50) NOT NULL, price decimal(10,2) NOT NULL, days int DEFAULT 1, cover varchar(255), description text, stock int DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id bigint PRIMARY KEY AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint NOT NULL, line_id bigint NOT NULL, quantity int DEFAULT 1, amount decimal(10,2) NOT NULL, status tinyint DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已完成, create_time datetime DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE comment ( id bigint PRIMARY KEY AUTO_INCREMENT, user_id bigint NOT NULL, line_id bigint NOT NULL, content varchar(500), score int DEFAULT 5, create_time datetime DEFAULT CURRENT_TIMESTAMP );两个加分的细节。第一个是订单表里用decimal(10,2)存金额而不是 double 或 float——浮点数算金额会出现 0.10.2 不等于 0.3 的经典事故答辩现场如果被问到这一点是很好的加分回答。第二个是订单号order_no不要用自增 id 直接暴露给用户建议用「yyyyMMddHHmmss 随机数」的方式生成既好看又能防止别人通过订单号猜测业务量。4.2 订单状态机与金额计算把逻辑写在 service 层而不是 controller 里订单是这个系统的业务核心也是最容易写出烂代码的部分。常见问题是把下单、支付、取消全写在 controller 里一个方法几百行。正确的做法是在 service 里定义状态流转方法把「查询线路、计算价格、扣库存」放到一个事务里。Service public class OrderService { Resource private OrderMapper orderMapper; Resource private LineMapper lineMapper; Transactional(rollbackFor Exception.class) public boolean createOrder(Order order) { TourLine line lineMapper.findById(order.getLineId()); if (line null || line.getStock() 0) { throw new RuntimeException(线路不存在或已售罄); } // 金额以数据库实时价格为准前端传的价格只做展示 BigDecimal amount line.getPrice().multiply(new BigDecimal(order.getQuantity())); order.setAmount(amount); order.setStatus(0); order.setOrderNo(generateOrderNo()); // 先扣库存再插订单防止超卖 int updated lineMapper.decreaseStock(order.getLineId(), order.getQuantity()); if (updated 0) { throw new RuntimeException(库存不足); } return orderMapper.insert(order) 0; } private String generateOrderNo() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%04d, new Random().nextInt(10000)); } }逻辑说明Transactional(rollbackFor Exception.class)保证扣库存和插入订单要么都成功要么都回滚。库存扣减用update tour_line set stock stock - #{num} where id #{id} and stock #{num}这种带条件的 SQL能在数据库层面堵住超卖很多同学直接把stock - num算好再传进去 update并发一上来就负库存属于必踩的坑。价格一定要以数据库中线路表的实时价格为基准计算不能信任前端传过来的金额这是支付系统的基本常识写进毕设里就是亮点。对订单状态的判断建议写一个状态变更方法用 if 判断或者 switch 都好关键是不允许乱跳比如待支付不能直接变成已完成。状态机不一定要用复杂框架代码里通过这种集中式校验就已经能表达清楚了。评论表和收藏表比较简单注意唯一索引评论表加uk_user_line唯一约束同类需求在毕设里经常会遇到——比如用户昵称必须唯一、一个商品只能收藏一次加唯一索引比在 service 里先查再插更可靠。5. 高分毕设避坑这 5 个坑我替你先踩了5.1 热更新不生效改了 HTML 不刷新必须手动重启现象IDEA 里改了templates下的 HTML刷新浏览器永远看不到变化必须重启 SpringBoot 才生效。有时候改 JS、CSS 也不生效浏览器里全是旧的。原因分两层。第一层是 Thymeleaf 模板缓存spring.thymeleaf.cachetrue是默认值模板在首次访问后被缓存后续请求不再读磁盘文件。第二层是 IDEA 的静态资源编译改了 resources 下的文件IDEA 默认不会自动把它复制到 target 类目录里所以热更新断了。解决application.yml里把thymeleaf.cache设为false同时确保依赖里有spring-boot-devtools。然后在 IDEA 设置里打开Build project automatically按CtrlShiftAlt/打开 Registry勾选compiler.automake.allow.when.app.running。这套组合才是 SpringBoot Thymeleaf 热更新的完整配方。如果改的是静态 JS 文件还伴随着浏览器缓存问题按 F12 打开控制台勾选 Disable cache或者在模板 JS 引用后面手动加版本号。5.2 登录拦截器把 Ajax 请求拦了返回了一整个 HTML 页面现象用户登录状态过期后页面里的异步请求全部走错误回调控制台打印的是 Thymeleaf 的登录页 HTML而不是 JSON 错误信息。前端解析resp.data.code直接报 undefined。原因拦截器对/api/**也做了登录校验未登录时调用了response.sendRedirect(/login)。普通页面跳转没问题但 Ajax 请求拿到的是 302 重定向到登录页的 HTMLaxios 里就变成了一坨字符串。解决拦截器里针对 Ajax 请求单独处理判断请求头X-Requested-With是否为XMLHttpRequest如果是对就写 JSON 响应而不是重定向。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(user) ! null) { return true; } // Ajax 请求返回 JSON普通页面跳转登录页 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); } else { response.sendRedirect(/travel/login); } return false; } }这里有个进阶处理在 WebMvcConfig 里注册拦截器时把登录、注册、静态资源、旅游线路展示接口都放行只拦截需要登录的路径比如订单、评论、收藏相关接口。5.3 静态资源 404明明文件在th:src 就是加载不出来现象启动项目后页面排版全乱F12 看 NetworkCSS 和 JS 全部 404。人眼一看文件明明在 static 目录里放着。原因三种可能性。最常见的是项目设置了context-path: /travel但模板里的资源路径没有加前缀比如直接写了/css/style.css结果浏览器去访问的是localhost:8080/css/style.css而不是/travel/css/style.css。第二种是你顺手改了spring.web.resources.static-locations导致默认的 classpath:/static/ 失效。第三种是 Maven 没有把 resources 下的文件同步到 target 目录启动类里MapperScan包写错这种事反而次要。解决模板里的静态资源一律使用th:src和th:href让 Thymeleaf 自动拼接 context-path。比如th:href{/css/style.css}渲染出的 HTML 会自动带上/travel前缀。这就是模板引擎相对原生 HTML 的一个实打实的好处别因为图省事写成href/css/style.css必炸。另外如果 devtools 热更新有时不刷新静态资源手动mvn clean一次再启动基本能解决。5.4 日期传到前端变成了时间戳老师看着那一串数字直摇头现象订单列表页展示创建时间页面上显示1720771200000这种数字而不是 2024-07-12 10:30:00。原因SpringBoot 默认使用 Jackson 序列化LocalDateTime 默认会被转成时间戳数组或字符串格式数据库里的datetime字段映射成java.util.Date时默认输出也是时间戳。解决在实体类的日期字段上加JsonFormat注解。同时把 application.yml 里 Jackson 的 date-format 配好双保险。JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-format: yyyy-MM-dd HH:mm:ss如果你用了 MySQL 的CURRENT_TIMESTAMP默认值查询出来是 Timestamp 类型也要确保实体里对应的是LocalDateTime而不是 String。这两个配置看起来小答辩演示时却非常显眼——页面上齐齐整整的时间格式比乱七八糟的数字串强太多。5.5 上线后 jar 包里的模板和静态资源全丢了现象本地 IDEA 跑得好好的mvn package打出来的 jar 包java -jar运行后页面疯狂报 404日志提示找不到模板 index.html。解压 jar 一看templates 目录是空的。原因Maven 默认只把src/main/resources下的文件打进 jar但如果你用了某些构建配置把 resources 目录排除掉了或者模板文件放在src/main/webapp且打包方式不是 war文件就进不去。常见场景是项目既有 Thymeleaf 又想支持 JSP手动动了 maven-war-plugin 的配置结果把默认行为改坏了。解决确认模板放在src/main/resources/templates静态资源放在src/main/resources/static这两个是 SpringBoot 的约定目录。如果实在放的乱在 pom 的 build 节点显式声明build resources resource directorysrc/main/resources/directory includes include**/*/include /includes /resource /resources /build还有一种一劳永逸的部署思路用 war 包丢进外部 Tomcat。把启动类继承SpringBootServletInitializer并重写configure方法打包成 war。这种部署方式对毕设演示有个好处——你可以在答辩现场把 war 包重新丢一遍 Tomcat整个过程可视化比黑匣子一样的 java -jar 更有演示效果。6. 打包部署与答辩演示从java -jar到 Tomcat 的完整链路6.1 jar 与 war 两种打包方式的选择jar 方式是默认行为mvn clean package后java -jar travel-0.0.1.jar就能跑适合自己写在文档里、给老师远程看。war 方式需要三步启动类继承SpringBootServletInitializer、pom 里把 packaging 改成 war、再排除内置 Tomcat 依赖避免和外部 Tomcat 的 Servlet 容器冲突。packagingwar/packaging dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency /dependenciesSpringBootApplication public class TravelApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(TravelApplication.class); } public static void main(String[] args) { SpringApplication.run(TravelApplication.class, args); } }注意war 包放进外部 Tomcat 的 webapps 目录后访问路径要带上 context-path。如果项目里设置的是/travel实际访问就是http://localhost:8080/travel/。如果 Tomcat 端口不是 8080改conf/server.xml里的 port别在答辩现场手忙脚乱。6.2 答辩演示前 10 分钟的验证清单演示环节最怕的是链路断了。我建议提前准备好一份自己的“预演脚本”按下面顺序过一遍每一项都是页面可以直观看到的注册一个新用户密码故意设置为123456和1234567两次确认有“密码不一致”提示这是前端校验的展示点。用新用户登录首页显示用户名而不是“登录”按钮证明 session 生效。进入线路列表页点第二页、搜索某个城市名确认请求走了 JSON 接口Network 面板里能看到/api/line/list的返回数据。点预订一双人份确认订单金额是单价乘以数量且数据库 stock 减了对应数量。在“我的订单”里取消订单再查一次库存确认库存加回来了——这是事务回滚的直观证明。最后展示application.yml里thymeleaf.cache: false说明你理解热更新机制而不是只会写代码。答辩时对着这个清单讲比空谈“我做了个旅游网站”可信得多。还有一个细节启动类上可以生成一个自定义 banner在 resources 下放banner.txt用 demo 启动画面提升第一印象。这不是核心功能但面试官或老师打开控制台时一个好看的 banner 会显得项目完成度更高。思路上尽量把“数据怎么流转”讲清楚模板渲染了哪些部分、接口返回了什么结构、订单金额为什么以服务端为准这三句话串起来这套系统就能稳稳地立住。希望这些踩坑经验能帮到你少熬几个无意义的夜。本文还有配套的精品资源点击获取