ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot旅游管理系统毕设实战:数据库设计、JWT登录与部署避坑

Spring Boot旅游管理系统毕设实战:数据库设计、JWT登录与部署避坑 简介一份基于SpringBootVue开发的旅游管理系统毕业设计源码包面向计算机相关专业毕业生、课程设计及期末大作业需求者也适合希望学习前后端分离项目的初中级开发者。系统覆盖管理员与用户双端包含景点信息、旅游线路、公告文章、变幻图、用户资料管理及系统设置等模块功能完整、界面直观可直接作为毕业设计或课程设计参考。压缩包共857个文件容量24.16MB核心由139个Java源码、51个Vue组件、47个HTML页面及配套CSS、JS构成另含SQL脚本和部署批次文件便于快速搭建运行环境。资源同时附有万字报告文档、部署说明与演示PPT代码包含注释并经过测试能够帮助读者快速理解项目结构、掌握SpringBootVue整合开发思路也可用于答辩展示与二次开发。目前已有238人学习下载适合需要完整项目方案、快速验证或深入研读源码的读者。1. 旅游管理系统毕业设计里最不会翻车的Java选题之一旅游管理系统在Java毕业设计里属于“不会惊艳全场但几乎不会翻车”的选题。游客注册登录、景点信息展示、线路下单、支付状态流转、后台管理维护这一套业务刚好把Java Web开发的核心知识点全部串起来Controller层接收请求、Service层写业务逻辑、DAO层操作数据库、JWT控制登录态。更关键的是它的业务复杂度适中一个人在一两个月内能独立做完答辩时导师从需求、数据库、部署三个方向追问你都有东西可答。适合两类人一类是Java基础能写增删改查、想在最短时间拿出完整可演示项目的应届生另一类是已经工作、需要用一套源码快速了解Spring Boot项目结构的开发者。这篇笔记按完整落地路径来讲先定技术选型和表结构再拆三个核心模块的实现思路最后把部署流程和典型踩坑讲透。2. 技术选型与项目结构骨架立不对后面每步都在返工2.1 Spring Boot 2.7还是SSM毕设场景下的选型逻辑先给结论能用Spring Boot就不用SSM。旅游管理系统里要处理登录拦截、事务、JSON返回、文件上传、异常统一处理SSM方案得写一堆spring-mvc.xml、web.xml的配置每加一个拦截器或数据源就要去翻XML出了问题排查链路特别长。Spring Boot用注解和自动配置把这些大部分收敛掉了你只需要关注application.yml和业务代码。版本组合上我一般推荐Spring Boot 2.7.x JDK 8/11 MySQL 5.7/8.0 MyBatis。这套组合有三个现实理由第一多数学校机房和毕业设计验收环境还是JDK 8你本机用JDK 17写的代码部署到实验室机器上可能直接跑不起来第二Spring Boot 2.7还在社区维护期内网上搜得到的插件、博客、踩坑帖基本都兼容它第三MyBatis的starter在2.7下用mybatis-spring-boot-starter 2.2.x就能稳定工作不需要处理Spring Boot 3带来的javax到jakarta坐标迁移。前端方案见表1你按自己的时间和答辩要求选。前端方案开发速度部署复杂度适合场景Thymeleaf Bootstrap快纯Java项目打成单个jar就能跑求稳、时间紧Vue 2 Element UI慢前后端分离要处理跨域前端要npm build后部署到Nginx想冲优秀毕设、界面加分这条笔记的实现讲解以Spring Boot Thymeleaf为主因为这是最容易完整落地、也最容易复现的路径。你要是选Vue方案后端接口设计照样能用只是多一层跨域配置和静态资源映射。2.2 数据库表设计五张核心表怎么建才像真实项目旅游管理系统的表不用建太多但每一张都要能讲清楚“为什么存在”。最常见的做法是五张核心表用户表、景点表、线路表、订单表、评论表。用户表管登录和角色景点表存旅游资源的静态信息线路表把多个景点组合成可售卖的产品订单表关联用户和线路并承载交易状态评论表做用户互动。订单表必须单独拆出来因为一个用户会买多条线路一条线路会被多个用户购买订单就是中间实体同时承担金额和状态字段。建表时要注意几个容易在验收时被导师问住的点一是密码字段不要存明文用BCrypt生成的60位哈希值存储二是金额字段用DECIMAL(10,2)而不是FLOAT浮点数计算金额会出现精度问题三是订单状态用varchar存中文枚举值比如“待支付”、“已支付”、“已取消”不要用0/1/2的数字导师看数据库一眼就能懂。下面是初始化的SQL脚本包含核心表的建表语句CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, phone VARCHAR(20) COMMENT 手机号, role VARCHAR(20) DEFAULT USER COMMENT 角色USER/ADMIN, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB COMMENT 用户表; CREATE TABLE t_scenic ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 景点ID, name VARCHAR(100) NOT NULL COMMENT 景点名称, city VARCHAR(50) COMMENT 所在城市, intro TEXT COMMENT 景点简介, image_url VARCHAR(255) COMMENT 图片URL, price DECIMAL(10,2) COMMENT 门票价格, open_time VARCHAR(50) COMMENT 开放时间说明 ) ENGINEInnoDB COMMENT 景点表; CREATE TABLE t_route ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 线路ID, name VARCHAR(100) NOT NULL COMMENT 线路名称, days INT COMMENT 行程天数, price DECIMAL(10,2) COMMENT 线路价格, scenic_ids VARCHAR(500) COMMENT 包含景点ID逗号分隔, cover_url VARCHAR(255) COMMENT 封面图URL ) ENGINEInnoDB COMMENT 旅游线路表; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, user_id BIGINT NOT NULL COMMENT 下单用户ID, route_id BIGINT NOT NULL COMMENT 线路ID, travel_date DATE COMMENT 出行日期, person_count INT DEFAULT 1 COMMENT 出行人数, total_amount DECIMAL(10,2) COMMENT 总金额, status VARCHAR(20) DEFAULT 待支付 COMMENT 订单状态, pay_no VARCHAR(64) COMMENT 支付单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, KEY idx_user_id (user_id), KEY idx_route_id (route_id) ) ENGINEInnoDB COMMENT 订单表;说两个容易被忽略的参数第一所有表都用ENGINEInnoDB它支持事务和外键innodb引擎在订单支付场景下才能保证数据一致性MyISAM是做不到行级锁和崩溃恢复的第二订单表的user_id和route_id要建普通索引否则后台按用户查订单时会全表扫描数据量上百条感觉不出来答辩时如果导师问“数据库怎么优化”你能答出索引这个点就是加分项。2.3 项目初始化pom.xml和application.yml一次配到位Maven依赖配置是整个系统的地基。Spring Boot 2.7.18这个版本号建议锁死不要用2.7.x通配避免某天Maven拉到一个有兼容问题的补丁版。核心依赖包含web、thymeleaf、mybatis、mysql驱动、lombok、jjwt这几组。具体配置如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /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.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependencies这里有个细节mybatis-spring-boot-starter 2.2.2对应的是MyBatis 3.5.x配合Spring Boot 2.7没有问题。jjwt要同时引入api、impl和jackson三个artifact否则运行时会报NoClassDefFoundError。application.yml配置里最需要注意的是时区和SQL日志。数据库连接串JDBC要在后面拼上serverTimezoneAsia/Shanghai不然MySQL 8的驱动默认按UTC解析时间订单创建时间会比本地时间差8个小时这个Bug排查起来非常隐蔽。配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travel.entity logging: level: com.travel.mapper: debugmybatis.mapper-locations指到classpath:mapper目录所有DAO接口的SQL映射XML都放在src/main/resources/mapper下。type-aliases-package配置后Mapper XML的resultType可以直接写User、Order这样的类名不用写全限定名。logging级别调成debug是为了本地开发时能在控制台看到每条SQL的执行情况排错时非常有用部署到生产环境前记得改回info。3. 核心模块实现登录、景点管理和订单流转扛住答辩的三块基石3.1 JWT登录与权限控制Token设计和拦截器实现旅游管理系统至少要区分普通用户和管理员两种角色。用户登录后后端签发一个JWT前端每次请求把它放在Authorization请求头里后端拦截器解析Token并判断角色。用JWT而不是Session是现在Java毕设的主流做法因为Session在小项目里没问题但答辩时导师很可能会问“换一台服务器Session怎么共享”你的思路是按无状态设计走用Token天然解决横向扩展问题。JWT工具类建议单独封装生成和解析的逻辑与业务代码隔离。代码如下Component public class JwtUtils { // 密钥要足够长这里用HS256要求至少32字节 private final String secret travel-management-system-secret-key-2025; // Token有效期单位毫秒这里设置为2小时 private final long expire 2 * 60 * 60 * 1000; public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }这里每一个参数都是有讲究的secret长度最少32字节否则HS256算法直接抛WeakKeyExceptionexpire设成2小时太短用户要反复登录太长又增加Token被截获后的风险claim里只放角色和userId不要放密码、手机号等敏感信息。写死密钥在毕设里可以接受但要记得在答辩时说明“生产环境应把密钥放到配置中心或环境变量”。有了工具类再写一个拦截器统一做登录校验。继承HandlerInterceptorAdapter的preHandle方法放行登录接口和静态资源其余接口必须校验Authorization头Component public class JwtInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims jwtUtils.parseToken(token.substring(7)); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }注意两点一是token.startsWith(Bearer )这个判断对应的前端在axios请求拦截器里要设置Authorization: Bearer ${token}两边字符串必须完全一致大小写和空格一个都不能错二是Token解析失败返回401前端要统一拦截这个状态码并跳转到登录页不要每个页面单独处理。需要管理员权限的接口只需要在Controller里校验request.getAttribute(role)是否等于ADMIN即可。3.2 景点管理模块Controller怎么写才规范景点管理是最典型的增删改查但答辩时导师看的是你代码是否分层合理。我见过不少同学把所有逻辑全写在Controller里一个方法七八十行这会被直接问“如果Service层要为多个Controller复用怎么办”。正确的拆法是Controller只做参数接收和结果返回Service写具体业务流程Mapper负责SQL操作。以分页查询景点为例Controller层代码Controller RequestMapping(/scenic) public class ScenicController { Autowired private ScenicService scenicService; GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, Model model) { PageResultScenic result scenicService.pageQuery(pageNum, pageSize); model.addAttribute(result, result); return scenic/list; } }Service层负责组装分页条件并调用MapperService public class ScenicServiceImpl implements ScenicService { Autowired private ScenicMapper scenicMapper; Override public PageResultScenic pageQuery(Integer pageNum, Integer pageSize) { // PageHelper做物理分页第一条执行后会拦截下一条SQL并拼上LIMIT PageHelper.startPage(pageNum, pageSize); ListScenic list scenicMapper.selectAll(); PageInfoScenic pageInfo new PageInfo(list); return new PageResult(pageInfo.getList(), pageInfo.getTotal()); } }这里用到了PageHelper分页插件它基于MyBatis拦截器实现核心使用方式就是startPage(pageNum, pageSize)必须紧跟要分页的查询语句中间不能插入别的查询操作否则分页SQL会作用到错误的语句上。PageInfo里的getTotal()返回总记录数getList()返回当前页数据这两个是前端分页组件要用的关键数据。景点列表页在Thymeleaf模板里通过th:each遍历result.list即可渲染这部分不做多讲。3.3 订单状态流转状态机设计是拿高分的隐藏点订单模块是整个系统里业务最重的部分也是答辩时最容易拉开档次的地方。很多毕设把订单状态做成一个随意的字符串字段直接在Controller里setValue导师追问“用户支付成功后异步回调来了怎么防止重复更新”时答不上来。规范的写法是定义订单状态枚举并把所有状态变更收敛到Service层禁止Controller直接改状态。订单状态枚举定义方式public enum OrderStatus { 待支付(待支付), 已支付(已支付), 已取消(已取消), 已完成(已完成); private final String desc; OrderStatus(String desc) { this.desc desc; } }Service层添加一个状态变更方法统一处理合法流转public Boolean changeOrderStatus(Long orderId, String targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new RuntimeException(订单不存在); } String current order.getStatus(); // 禁止非法跳转已支付不能变回待支付已取消不能变成已完成 if (已支付.equals(current) 待支付.equals(targetStatus)) { throw new RuntimeException(支付状态不可回退); } if (已取消.equals(current) !已完成.equals(targetStatus)) { throw new RuntimeException(当前状态不可变更); } order.setStatus(targetStatus); order.setPayNo(TRAVEL System.currentTimeMillis()); return orderMapper.updateById(order) 0; }为什么要这么做因为订单状态是一个业务规则密集的字段分散更新意味着每条变更逻辑都要重新校验上一状态代码里出现遗漏的概率很大。集中在一个方法里无论从Controller、定时任务还是支付回调进来都走同一套校验逻辑这在答辩时可以说是“借鉴了状态机思想”。还有一个容易被追问的点用户在支付页面重复点击“确认支付”怎么办。只靠前端禁用按钮不够Service层要在进入支付逻辑前加一个对当前状态是否为“待支付”的判断不是待支付直接拒绝本次操作。4. 部署全流程与典型避坑能跑起来才算真正做完4.1 本地部署验证Maven打包加命令行启动拿到源码后第一件事不是直接往服务器上传而是先在本地把项目完整跑一遍。我见过太多人跳过本地验证直接部署服务器结果日志里全是ClassNotFound最后只能在服务器上边猜边改效率极低。本地验证分三步走。第一步确认环境干净。在终端执行java -version查看JDK版本mvn -version看Maven版本。版本不匹配是第一个大坑项目的pom.xml是Java 8编译级别你的本机却只有JDK 17且没有配置JAVA_HOME指向旧版本编译时会有兼容问题。常见做法是用JDK 8安装包配置好环境变量这里注意Windows的JAVA_HOME要填到JDK根目录而不是bin目录Path里再加%JAVA_HOME%\bin配好后新开一个终端窗口验证。第二步创建数据库并导入SQL。在MySQL里执行CREATE DATABASE travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后用source命令导入项目里的schema.sql和data.sql。这里强调utf8mb4而不是utf8因为在MySQL 8里utf8mb3最多支持3字节字符一些保存表情或生僻字的数据会报错utf8mb4才能完整覆盖Unicode。第三步打包并启动。在项目根目录执行以下命令mvn clean package -DskipTests这条命令先把target目录清空然后重新编译、打成一个可执行的jar包。-DskipTests是跳过单元测试毕设项目里一般没有完整的测试类但保留这个参数可以避免测试代码报错导致打包失败。打包成功后在target目录下执行java -jar travel-system-0.0.1-SNAPSHOT.jar启动后观察控制台日志看到“Started TravelSystemApplication”即表示启动成功。然后浏览器访问http://localhost:8080如果页面能正常打开说明本地部署已经完成下一步才轮到服务器部署。4.2 Linux服务器部署java环境配置与systemd守护进程服务器部署的核心差别在于两点环境靠命令行配置进程守护要用systemd而不是窗口方式。先装JDK 8和MySQL这里用一个相对不易出错的顺序先把JDK的tar.gz包解压到/usr/local/java然后配置环境变量接着启动MySQL并创建数据库最后部署jar包。以CentOS 7/8环境为例配置java环境变量的操作mkdir -p /usr/local/java tar -zxvf jdk-8u202-linux-x64.tar.gz -C /usr/local/java cat /etc/profile EOF export JAVA_HOME/usr/local/java/jdk1.8.0_202 export PATH\$JAVA_HOME/bin:\$PATH EOF source /etc/profile java -version第三步里反斜杠转义$PATH的作用是让变量在追加到文件时保持“变量引用”状态而不是当场被展开成当前终端的PATH值这是一个容易忽略的细节。执行source /etc/profile后java -version输出1.8开头的版本号就说明环境变量生效。jar包部署时不要直接nohup java -jar放后台就不管。我一般会把它注册成systemd服务这样服务器重启后项目能自动拉起进程崩溃也能自动重启。服务文件配置如下[Unit] DescriptionTravel System Afternetwork.target mysql.service [Service] Typesimple Userroot WorkingDirectory/opt/travel ExecStart/usr/local/java/jdk1.8.0_202/bin/java -jar /opt/travel/travel-system.jar Restartalways RestartSec10 [Install] WantedBymulti-user.targetWorkingDirectory指定成jar包所在目录很重要如果你的代码里有相对路径读写文件比如上传图片保存到./upload进程的工作目录决定了这个文件夹从哪里创建。Restartalways配合RestartSec10表示进程异常退出后10秒自动拉起Mad使用jstack抓线程栈就能看到状态。上传目录和数据权限也要注意如果你的项目涉及文件上传一定要在application.yml里配置一个独立的上传路径并设置777权限。4.3 五个典型踩坑现象、原因、解决坑一数据库连接报Access denied或Communications link failure。现象是项目启动时连不上MySQL报错提示用户认证失败或无法连接。原因通常是数据库密码和application.yml配置不一致或者MySQL服务没启动。解决方法是先在MySQL命令行里用配置的用户名和密码登录验证再确认MySQL端口3306处于监听状态最后检查JDBC连接串的IP和端口。坑二页面样式丢失、跳转404。现象是启动成功后访问首页有HTML但没有CSS样式刷新所有路径都返回404。原因多半是Thymeleaf模板找不对静态资源路径或者把页面放在templates目录外了。解决方法是确认所有HTML页面都在src/main/resources/templates下静态资源在static下Controller返回的逻辑视图名要能和模板文件对应上。坑三JWT Token解析时报签名不匹配。现象是用户登录成功后再发第二次请求就报401。原因是生成Token和解析Token使用的密钥不一致最常见的是把带引号的secret字符串存到了配置中心读取时多了引号字符导致字节内容不同。解决方式是写一个单元测试用例用同一份密钥生成再解析确认能通过再排查哪里改了密钥。坑四上传的图片文件找不到。现象是后台管理上传了景点图片页面显示400或浏览器访问图片路径404。原因是Linux环境下没有对应的上传目录或者目录权限不是775。解决方法是在启动前创建好upload目录并chmod 777同时检查图片URL返回的相对路径能否被静态资源映射到。坑五mysql-connector-java版本和MySQL服务版本差距过大。现象是启动时报Unknown character set或协议握手失败。原因是项目依赖的MySQL驱动太旧、服务器MySQL版本太新。解决方法是统一用mysql-connector-java 8.0.28版本并在连接串中不加useSSLtrue这个参数本地连数据库时SSL会拖慢连接速度报错也会干扰排查。5. 从能跑到能答辩三个必改点和一份自检清单拿到毕设源码包之后最忌讳的事情是原封不动运行、原封不动交。很多同学直接照着PPT里的截图和报告的文字交上去结果导师从自己学院的知网查重系统里一比对重复率能达到七八成。这套源码、万字报告、部署说明和PPT本质上是给你画了一个框架和底稿你要做的是把里面的内容替换成自己的理解。第一个必改点是数据库表和字段。把t_scenic改成自己定义的scenic_spot也好加一个用户收藏表也好要让表结构里有至少一个模块是你自己设计的。第二个必改点是界面文案和Logo这是成本最低的差异化改造把系统的标题改成你自己的课题名前端页面加一条你自己设计的样式。第三个必改点是报告里的系统分析部分分解清楚哪些是原有的、哪些是你二次开发的。比如你给订单加了一个定时取消的超时任务这就是一个可以重点讲的加分功能对应的表、接口、界面全链路都要能自圆其说。最后是答辩前要做的一份自检清单# 1. 本地依赖是否完整从clone目录到启动不超过30分钟 mvn clean package -DskipTests java -jar target/travel-system.jar # 2. 核心演示路径是否走通 # 用户注册 - 登录 - 浏览景点 - 选择线路下单 - 模拟支付 - 订单列表显示已支付 # 3. 管理员入口是否可用 # 管理员登录后进入后台 - 景点增删改查 - 订单状态人工修正 - 查看全部用户 # 4. 数据库数据是否足够演示 # 至少准备10个景点、8条线路、5个不同状态的订单 # 5. 报告关键章节是否和代码对应 # 数据库设计章节里的表名要和SQL文件一致功能模块章节的截图要重新截我自己的习惯是答辩前一天不看代码而是打开PPT把每个功能页面讲一遍看哪些地方卡壳就重点补哪块。这个办法帮我发现过不少认知盲区比如讲订单模块时突然想起“重复支付防护”没写赶紧回去补上。旅游管理系统作为毕设的性价比相当高关键是把每一层逻辑都吃透而不是停留在“能跑就行”。希望这份拆解能帮你在做毕设的路上少走几步弯路。本文还有配套的精品资源点击获取
返回列表