简介在Web应用开发中SpringBoot作为主流Java后端框架与Thymeleaf模板引擎、MySQL数据库组合构成了经典的服务端渲染技术栈。其核心原理在于通过控制器将业务数据封装到Model再由Thymeleaf在服务端动态渲染HTML页面实现前后端一体化交付。这种架构不仅降低了对前端工程化和跨域处理的依赖还保留了清晰的Controller-Service-Mapper分层特别适合中小型业务系统的快速构建。在旅游信息化场景中基于该技术栈开发的景点与酒店预订网站能够覆盖用户注册登录、景点/酒店浏览、在线下单、订单管理、评论互动等完整业务闭环为毕业设计或课程实践提供可运行的工程样板。本文即围绕这类系统的数据库设计、核心代码实现、部署运维及常见问题展开给出可落地的SpringBoot实战参考。 每年四月份开始就会有不少学生朋友来问毕业设计的事。问得最多的一类就是某某管理系统、某某预订网站。今天这篇我直接拿一个真实能跑的项目来说——基于SpringBootThymeleafMySQL开发的旅游景点酒店预订网站Java完整源代码和数据库脚本都包含在内。它不是网上那种只有半截代码的Demo而是一套从用户注册登录到下单预订完整闭环的项目基本覆盖了毕设答辩时老师最爱问的几个点。这套项目的好处在于技术栈主流界面不花哨但够用数据库表结构清晰业务逻辑不复杂但五脏俱全。不管是做毕设、课程设计还是想通过一个完整项目来理解SpringBoot后台开发的整个链路都非常合适。下面我就从设计思路、数据库、代码实现、部署运维几个维度把这个项目彻底拆开讲。1. 项目整体设计与思路拆解1.1 项目到底做了什么事旅游景点酒店预订网站核心业务不复杂。用户进到网站之后能浏览旅游景点和酒店列表可以查看详情然后选择日期和数量进行预订。预订之后在个人中心能查看自己的订单记录可以对已消费的景点或酒店进行评价。管理员则通过后台维护景点信息、酒店信息、用户状态和订单状态。这里有一个很关键的取舍到底是做成“旅游平台”这种多商家入驻的模式还是做一个“单站管理”的模式。毕设项目里我建议做成单站管理就好。也就是说景点和酒店都直接由管理员在后台添加维护用户只能浏览和下单。这样既能把业务闭环做完整又不会因为引入商家端、结算体系而把项目复杂度无限放大导致代码写不完、论文也没法聚焦。1.2 为什么选SpringBootThymeleaf而不是前后端分离这个项目名字里就写了SpringBootThymeleaf那自然要聊清楚为什么这么选。很多同学现在一上来就想着VueSpringBoot前后端分离觉得这样才“高级”。但实际上毕设答辩时老师更关注的是你能不能把一个完整业务跑通能不能把设计逻辑讲清楚。Thymeleaf是一种服务端模板引擎它能在HTML页面里直接渲染Java后台传来的数据。选它有几个现实原因。第一项目结构简单不需要单独启动前端工程也不需要处理跨域问题。第二对于“页面数量不多、交互不复杂”的预订网站来说服务端渲染的体验并不差页面加载快而且写起来直观。第三和SpringBoot整合非常顺滑只要在pom.xml里引入依赖再把页面放在templates目录下Controller返回的逻辑视图名就能自动对应到模板文件。对新手来说这种“看得见摸得着”的渲染方式比前后端分离那种页面和数据分离的调试方式友好得多。我见过不少同学用前后端分离做毕设结果答辩现场前端跨域问题没解决页面数据出不来。如果你做的不是那种强交互、多状态管理的大型系统老老实实上Thymeleaf反而更稳。1.3 技术选型的底层逻辑除了SpringBoot和ThymeleafORM层面我建议用MyBatis-Plus。它本质上还是MyBatis只是帮我们把单表的增删改查都封装好了不需要写一大堆XML文件。这个项目里的用户表、景点表、酒店表、订单表基本都是单表操作用MyBatis-Plus可以省掉大量重复代码让你把精力放在业务和页面上。如果你在简历里写了了解MyBatis-Plus这也是一个不错的加分点。数据库用MySQL这也是最稳妥的选择。版本上建议5.7或者8.0都可以但要注意8.0的驱动名和连接URL中存在一些时区和驱动类的差异后面我会专门讲坑。前端样式用Bootstrap不自己造轮子也不用维护复杂的Vue组件。因为这是一个毕设项目保证界面干净、布局合理、能正常响应就足够了。2. 功能模块划分与数据库设计2.1 前台和后台的功能边界系统面向两类角色普通用户和管理员。普通用户的功能集中在网站前台注册、登录、退出登录浏览景点列表、酒店列表查看景点详情、酒店详情提交景点门票预订、酒店房间预订在个人中心查看订单列表对订单进行取消或删除操作对已“完成”的订单发表评论管理员的功能集中在后台管理端管理员登录景点信息管理新增、编辑、上下架、删除酒店信息管理新增、编辑、上下架、删除用户管理查看用户列表、禁用/启用账号订单管理查看所有订单、按状态筛选、修改订单状态评论管理查看评论、删除违规评论这里有一个容易被忽视但很重要的点前台和后台不要各写一套登录逻辑。我处理的方式是在用户表里加一个role字段1表示普通用户0表示管理员。登录时根据角色跳转到不同首页同时用一个拦截器做权限控制。这样做数据模型简单代码量少而且答辩时你能讲清楚“同一个用户体系如何通过角色字段区分不同身份”。2.2 数据库表结构设计整个项目用到的数据表我拆成了8张分别是用户表、景点表、酒店表、订单表、评论表、收藏表、公告表和分类表。这里我挑重点讲。用户表CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 密码建议MD5或BCrypt, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint(1) DEFAULT 1 COMMENT 0管理员 1普通用户, status tinyint(1) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;景点表和酒店表结构类似核心字段有名称、简介、图片、地址、价格、开放时间/房型、状态等。这里要注意图片字段存的是URL路径不是图片本身。很多新手会把图片转成Base64往数据库里存一旦图片多了数据库体积暴涨页面渲染也会变卡。正确做法是图片上传后存到本地某个目录或OSS数据库里只存访问路径。订单表是最关键的一张表因为景点门票和酒店预订本质上都是“下订单”这个动作。我把两类订单合成一张表通过product_type字段区分是景点(1)还是酒店(2)。这样做的好处是订单管理后台只有一套代码不需要写两套。缺点是如果以后要做复杂的订单扩展单表会有点拥挤但对于毕设来说足够。订单表核心字段CREATE TABLE t_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id int(11) NOT NULL COMMENT 下单用户, product_type tinyint(1) NOT NULL COMMENT 1景点 2酒店, product_id int(11) NOT NULL, product_name varchar(100) NOT NULL, price decimal(10,2) NOT NULL COMMENT 单价, num int(11) NOT NULL DEFAULT 1 COMMENT 数量, total_price decimal(10,2) NOT NULL, book_date varchar(20) DEFAULT NULL COMMENT 游玩/入住日期, contact_name varchar(50) DEFAULT NULL, contact_phone varchar(20) DEFAULT NULL, status tinyint(1) DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样的设计已经能够支撑一个完整的预订流程用户在前台选择景点或酒店填写联系人信息和预订日期提交订单管理员在后台确认或完成订单用户看到状态变化后可以评论。2.3 SQL脚本里的那些细节数据库脚本不是把建表语句一放就完事了还要考虑初始化数据和兼容性。第一字符集一定要用utf8mb4而不是utf8。因为utf8在MySQL里最多存3字节像一些生僻字或者emoji表情会存不进去。虽然网站上可能用不到emoji但用户评论区保不准有人粘贴一个特殊符号用了utf8mb4就不会出现乱码或报错。第二密码字段不能明文存。虽然我这里为了演示写的是简单方式但你在毕设里最好用BCrypt或者至少MD5加盐。答辩时如果老师问“你的密码安全吗”你至少能说出我做了加密处理。第三初始化数据里一定要有一条管理员账号。很多同学辛辛苦苦把项目跑起来结果后台登录入口不知道账密。我的脚本里固定初始化一个admin/admin123并在文档里写清楚这样拿到项目的人第一分钟就能进后台。3. 核心代码实现与关键环节3.1 项目目录结构与启动入口项目使用Maven管理依赖结构如下src/main/java/com/example/travel ├── TravelApplication.java ├── controller ├── service ├── mapper ├── entity ├── config ├── interceptor └── common启动类没什么特别SpringBootApplication MapperScan(com.example.travel.mapper) public class TravelApplication { public static void main(String[] args) { SpringApplication.run(TravelApplication.class, args); } }注意点有两个。第一MapperScan一定要写否则MyBatis-Plus的Mapper接口不会被扫描到启动会报找不到Bean。第二SpringBoot启动时如果你遇到端口被占用可以临时在application.yml里改server.port或者直接杀掉占用进程。application.yml中的关键配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root thymeleaf: cache: false servlet: multipart: max-file-size: 10MB开发阶段记得把Thymeleaf的缓存关掉不然改一个HTML页面要重启整个项目才能看到效果极其浪费时间。3.2 实现一条完整的酒店预订流程以酒店预订为例完整链路是这样的用户进入酒店列表页点击详情页选择房型、入住日期、入住天数填写联系人和手机号提交订单后台生成一条订单记录。实体类TableName(t_hotel) public class Hotel { TableId(type IdType.AUTO) private Integer id; private String name; private String address; private String price; private String roomType; private String images; private String description; private Integer status; private Date createTime; // getter/setter 省略 }Mapper接口一行代码都不用写public interface HotelMapper extends BaseMapperHotel { }Service层做基本的列表分页和详情查询Service public class HotelService { Resource private HotelMapper hotelMapper; public IPageHotel getHotelPage(int pageNum, int pageSize) { PageHotel page new Page(pageNum, pageSize); QueryWrapperHotel wrapper new QueryWrapper(); wrapper.eq(status, 1).orderByDesc(id); return hotelMapper.selectPage(page, wrapper); } }Controller层控制请求跳转和数据绑定Controller RequestMapping(/hotel) public class HotelController { Resource private HotelService hotelService; GetMapping(/list) public String list(RequestParam(defaultValue 1) int page, Model model) { model.addAttribute(page, hotelService.getHotelPage(page, 8)); return hotel/list; } GetMapping(/detail/{id}) public String detail(PathVariable Integer id, Model model) { model.addAttribute(hotel, hotelService.getById(id)); return hotel/detail; } }这里很多人会纠结Controller层我要不要写Service直接Mapper不行吗从功能上确实可以直接注入Mapper但这样会把业务逻辑和Web层耦合在一起。答辩时老师可能会问“你们项目分了几层”如果Controller里直接操作数据库就尴尬了。所以哪怕麻烦一点也要保留Controller-Service-Mapper这条链路。Thymeleaf页面渲染时列表页核心代码长这样div classrow th:eachhotel : ${page.records} div classcol-md-4 div classcard img classcard-img-top th:src${hotel.images} alt div classcard-body h5 classcard-title th:text${hotel.name}酒店名称/h5 p classcard-text th:text${hotel.address}地址/p p classcard-text text-danger th:text${hotel.price} 元/晚价格/p a classbtn btn-primary th:href{/hotel/detail/{id}(id${hotel.id})}查看详情/a /div /div /div /div提交订单的Controller接受一个表单对象生成订单号并保存PostMapping(/submit) public String submit(Order order) { order.setOrderNo(T System.currentTimeMillis()); order.setStatus(0); order.setCreateTime(new Date()); orderService.save(order); return redirect:/order/my; }订单号用时间戳生成简单但不严谨如果并发量大会重复。毕设里够用。答辩时你可以说生产环境会改用“日期随机数自增序列”的方式。3.3 Thymeleaf页面渲染的几种常用姿势Thymeleaf最常见的用法就是th:text、th:href、th:each、th:if但有几个细节值得注意。th:each遍历时如果还想拿到下标可以这样写tr th:eachorder, iter : ${orderList} td th:text${iter.count}1/td /trth:if条件判断通常用来做状态显示切换span th:if${order.status 0} classbadge badge-warning待确认/span span th:if${order.status 1} classbadge badge-success已确认/spanth:inline可以直接在JavaScript里获取后端数据这个用起来很香。比如地图页面或者统计页面有时候需要把后端列表转成JS数组script th:inlinejavascript var hotelList [[${hotelList}]]; console.log(hotelList); /script但要小心如果数据量较大这种内联方式会把数据直接输出到HTML源码里。毕设没问题生产环境要注意接口脱敏。3.4 登录拦截与权限控制预订网站里有两个操作一定需要用户登录提交订单和个人中心。管理员后台则必须要求role0。SpringBoot里最方便的方式是用HandlerInterceptor。先定义拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }再定义管理员拦截器在用户登录基础上再校验角色public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user (User) request.getSession().getAttribute(loginUser); if (user null || user.getRole() ! 0) { response.sendRedirect(/login); return false; } return true; } }然后在配置类里面注册拦截路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/order/**, /user/**); registry.addInterceptor(new AdminInterceptor()) .addPathPatterns(/admin/**); } }这里要注意静态资源路径比如/css/, /js/, /images/**通常不会被拦截因为它们不是Controller路径但如果有自定义资源映射最好也在拦截器里排除掉。4. 数据库脚本的导入与初始化4.1 适合新手的数据库准备方式拿到项目后第一件事就是建库导数据。我推荐用Navicat或者DataGrip也可以用IDEA自带的Database面板。如果你电脑上还没装MySQL建议直接装MySQL 8.0版本网上有官方安装包。数据库脚本文件通常是.sql格式里面包含了CREATE DATABASE、CREATE TABLE和INSERT语句。用Navicat导入时右键连接选择“运行SQL文件”选到项目里的travel.sql即可。导入完成后刷新表列表能看到8张表就已经成功了。这里有个小坑很多从网上下载的项目SQL脚本里写的字符集是老旧的latin1导入后中文全是乱码。所以我的脚本开头固定加上SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;这样能最大程度避免乱码和外键约束导致的导入失败。另外如果你用的是MySQL 8.0而项目连接URL里没有serverTimezoneAsia/Shanghai启动时大概率会报时区错误。4.2 IDEA数据库工具导入脚本对于用IDEA开发的同学其实不需要额外装Navicat。IDEA右侧的Database面板可以直接连接MySQL。连接成功之后选中你的数据库右键“Run SQL Script”选择travel.sql就会自动执行。这种方式的好处是SQL执行日志会在IDEA控制台里展示如果某条语句报错你能直接看到是哪张表、哪个字段的问题。如果项目自带的SQL脚本是旧版本的导入MySQL 8.0时可能会遇到“datetime”类型默认值的问题。老版本的SQL语句里如果有create_time datetime DEFAULT 0000-00-00 00:00:00这种写法在MySQL 8.0的严格模式下会直接报错。解决方案有两个一是把默认值改成NULL二是启动时去掉sql_mode里的NO_ZERO_DATE。我的项目脚本中已经用DEFAULT NULL不会触发这个问题但如果你从别处拿的脚本要特别留意。4.3 初始化数据与实际运行效果脚本里不仅要有表结构还要有几条能看得见的业务数据。比如景点表至少放5条国内知名景点酒店表放3-5条不同价位的酒店用户表放一个admin。这些数据在第一次打开网站时就能看到效果而不是白屏一张。初始化数据尽量真实一些。比如景点表的图片路径可以直接用网上可访问的图片URL也可以先放在本地static/images目录下。如果图片在外面项目迁移后不会丢如果在本地离线演示时也能加载。我建议做两手准备脚本里存相对路径/images/scenic/xxx.jpg同时把示例图片打包在项目里。这样无论是演示还是答辩都不会因为网络问题导致图片显示不了。我个人在导入脚本后还会做一次“冒烟测试”启动项目前台注册一个测试用户提交一笔酒店订单再到后台确认订单。如果这一条链路能走通说明数据库、后端、前端模板基本都没问题。剩下的事情就是按自己的需求去改页面和功能了。5. 从部署到答辩的实战经验5.1 本地运行的正确姿势拿到项目后第一件事不是急着改代码而是先把项目跑起来。第一步确保JDK版本和项目匹配。SpringBoot 2.x通常要求JDK 8或11SpringBoot 3.x要求JDK 17以上。我的项目基于SpringBoot 2.7用JDK 8最稳。有些同学电脑装的是JDK 18启动时可能会报“IllegalArgumentException”或者一些奇怪的类加载错误。如果遇到这种问题优先把JDK版本降下来。第二步Maven配置。国内网络环境下面pom.xml第一次加载依赖会比较慢。建议在Maven的settings.xml里配置阿里云镜像。不然卡在下载依赖半个多小时体验极差。第三步启动参数。在IDEA中直接运行TravelApplication类的main方法即可。如果控制台输出“Tomcat started on port(s): 8080”说明启动成功。然后浏览器访问http://localhost:8080就能看到网站首页。这里再提醒一个常见问题如果你修改了application.yml中的数据库密码但忘记删掉target目录下的旧classes文件可能出现配置不生效的情况。IDEA中遇到这种诡异问题先执行mvn clean再重新启动。5.2 常见问题排查速查表我把这些年帮人调试类似项目时遇到的最高频问题整理成一张表供大家参考。问题现象原因分析解决方式启动报错无法连接数据库application.yml里的用户名密码与本地MySQL不一致核对MySQL账号密码或修改配置页面中文乱码数据库连接URL缺少characterEncodingutf8或数据库字符集不是utf8mb4修改JDBC连接参数确认数据库表字符集点了提交订单没反应登录拦截器把请求拦截了但页面没有跳转提示检查拦截器路径配置确认用户已登录修改了HTML页面不生效Thymeleaf缓存启动未关闭在application.yml中设置cache: false并重启图片无法显示图片路径不存在或静态资源配置错误检查static目录结构和images字段值端口被占用8080端口被其他程序占用使用netstat或IDEA提示关闭进程或改server.port调用后台接口返回Whitelabel Error页面路径和Controller返回值不对应检查templates目录下模板路径是否与逻辑视图名一致数据库脚本导入报错表顺序不对存在外键依赖先跑SET FOREIGN_KEY_CHECKS0; 或按表依赖顺序导入还有一点如果你的项目在别人电脑上跑得好好的到自己电脑上报错90%是环境问题而不是代码问题。JDK版本、MySQL版本、Maven版本一个一个核对别上来就怀疑源码有bug。5.3 打包部署到云服务器现在很多学校答辩会要求“现场演示”但有些同学觉得自己电脑带过去太麻烦或者担心现场网络不好会提前把项目部署到云服务器上。我也建议这么做因为线上访问演示比本地方便得多。打包命令很简单mvn clean package构建完成后target目录下会生成一个travel-0.0.1-SNAPSHOT.jar。在服务器上安装好JDK和MySQL导入SQL脚本后启动java -jar travel-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod如果不需要多环境直接把application.yml里的数据库地址改成服务器上的就行。这里有个坑如果用云服务器防火墙要开放8080端口同时MySQL要允许远程连接。MySQL默认只监听127.0.0.1需要在my.cnf里注释掉bind-address并给root账号授权远程访问。但注意这是开发环境做法真实生产环境千万不要把root开给公网低版本和弱口令很容易出问题。6. 项目扩展建议6.1 代码层面的进一步优化如果你想让这个项目在答辩时更有亮点可以从几个方向扩展。第一把密码从简单的MD5改成BCrypt加密。Spring Security里自带的BCryptPasswordEncoder就能用不用引入完整的Spring Security模块。改完之后注册和登录逻辑都走加密校验数据库里存的是加密串看起来更专业。第二增加接口参数校验。比如订单提交时联系人手机号可以用正则校验价格字段用DecimalMin限制。SpringBoot自带的validation依赖就能做代码量不大但能体现细节。第三把硬编码的常量抽成枚举或者配置类。比如订单状态0/1/2/3散落在代码里到后期很容易记混。定义一个OrderStatus枚举代码可读性和可维护性都会好很多。6.2 业务功能层面的扩展旅游网站还有一个常见功能是搜索。现在很多毕设版的列表页只有分页没有关键词搜索。加一个搜索功能其实不难在Controller里接收一个keyword参数传给Service层时用QueryWrapper的like条件。比如QueryWrapperScenic wrapper new QueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), name, keyword);这样列表页就可以通过搜索框过滤景点和酒店。很实用展示也多一个点。另一个值得扩展的是数据统计。后台加一个简单的统计面板显示今日订单数、累计用户数、热门景点Top5。我建议用ECharts画一个柱状图数据可以从订单表里按产品ID分组统计。这部分代码不多但答辩效果非常好。因为老师一看你不仅会增删改查还能做简单数据分析和可视化印象分会高不少。6.3 给即将答辩的同学一些经验最后再分享一个实际体会项目能跑只是起点能讲清楚才算完成。答辩前花点时间梳理一下典型问题比如“你这个系统和其他预订网站有什么区别”“订单状态是怎么流转的”“如果并发下单你怎么处理”。不需要记长篇大论能用一两句话把自己的设计逻辑讲明白就行了。我当时做类似项目时提前准备了一张系统架构图从用户浏览器到Controller到Service到Mapper再到数据库每一步都标好。答辩的时候照着图讲一遍整个流程老师点点头就知道你是真做了项目的不是只把别人的代码跑起来。如果你还能现场演示一下“禁用某个用户后这个用户登录不了”的细节那基本就稳了。本文还有配套的精品资源点击获取