ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot旅游网站毕设全攻略:从选型到部署一次讲透

Spring Boot旅游网站毕设全攻略:从选型到部署一次讲透 计算机毕业设计项目里基于 Spring Boot 的旅游网站属于那种看着普通、做起来却特别稳的选题它业务链条完整、前台后台都有、页面多而不难答辩时随便挑一个功能都能讲得清楚。每年我都会把它放到毕设推荐名单的前排不是因为新潮而是因为毕业设计最怕的其实是两个坑——想做的太大做不完和做完了没什么可讲。旅游网站恰好两头都不沾。这篇文章我就以一个带过不少毕设项目的人的身份把我自己的选题理由、技术选型、数据库设计、核心功能写法和踩坑记录全部摊开来讲适合拿着这个题目不知道从哪下手的同学也适合已经写了一半、正在跟日期序列化或者上传图片较劲的朋友直接跳到对应章节。1. 毕设选题的“安全牌”旅游网站到底赢在哪里1.1 为什么这个题目不容易翻车很多学生一开口就想做商城因为觉得电商系统通用、有说服力。但电商里光是SKU、库存、优惠券、物流状态就能把一个人拖进泥潭三个月能做出一套能看的完整商城的人我见得不多。旅游网站不一样它保留了电商最核心的浏览-下单-支付-评价闭环但商品模型简单景点、线路、酒店每个就是一个对象顶多加一个日期和余票量把复杂度控制在了一个毕设选手能真正驾驭的范围。另外很重要的一点旅游网站的展示性质很强。景点详情页、线路图文介绍、酒店房型列表这些东西天然需要大量图片和结构化数据做好了整套系统的观感非常加分。答辩台下坐着的不只是看你代码的人更多是先看你界面的人。界面正常、数据不假基本就赢了一半。1.2 技术栈选型Spring Boot版本和持久层框架怎么定先给一套我比较推荐的稳妥型组合后面按这个讲层面推荐选型说明开发语言/JDKJava 8兼容性最好教师机和自己电脑都不容易出幺蛾子后端框架Spring Boot 2.7.x别碰3.xJDK17门槛和依赖兼容性对毕设是纯负担持久层MyBatis-Plus 3.5.x单表CRUD省时间复杂多表还能手写SQL兜底数据库MySQL 5.7 / 8.0教辅资料多Navicat直接连就行模板引擎Thymeleaf前后端不分离一个Spring Boot工程打成一个jar就能跑缓存可选Redis Spring Data Redis做一个景点详情缓存就算加分项不做也不扣分为什么我不推荐Spring Boot 3.x不是新技术不好而是你大概率要用的MyBatis-Plus、部分教程里的配置方式老版本只有文章边界问题一旦出现你排查的成本会非常高。毕设的目的是完整地做出一个系统、把原理讲清楚不是给社区测bug。Thymeleaf和Vue前后端分离怎么选如果前端能力一般Thymeleaf最省心后端渲染、不用起两个服务、部署时一个jar搞定。如果你对Vue确实熟也可以做成前后端分离Spring Boot换成只出JSON的REST风格工作量会多出联调和跨域配置这一块要自己核算时间。我遇到的大多数同学老老实实用Thymeleaf反而最后效果最好。1.3 工程结构怎么搭才干净后端包结构我习惯这样com.example.travel ├── TravelApplication.java ├── common // Result统一返回、异常处理、常量 ├── config // 拦截器、资源映射、MyBatis-Plus分页插件 ├── controller // 前台和后台的Controller ├── service // 业务接口和实现 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 前端参数对象登录传参、下单传参 ├── vo // 视图对象详情展示、后台列表 ├── utils // OrderNoUtil等工具类这个分层不是为了好看是为了答辩时老师说你的项目结构怎么样你能说出话。Controller只收参数调ServiceService里做事务和业务判断Mapper只处理SQLEntity和数据库字段一一对应VO长什么样由页面决定。写清楚了一层后面所有功能都往固定的格子里放反而快。Response统一返回可以搞一个简单的Result类public class ResultT { private Integer code; private String message; private T data; // 静态方法 ok()、fail() }再用RestControllerAdvice做个全局异常处理器把业务异常比如余票不足订单已取消统一抛出来转成JSON。这件事看着小但我带过的项目里凡是前期没做统一返回的到写前端联调时全是囫囵吞枣式debug非常拖进度。2. 先把业务和表结构想清楚五个核心实体与订单状态机2.1 需求边界前台六件事、后台五件事写代码最忌讳拿着题目直接建表。我一般先做一张功能清单把哪些必须做、哪些是加分项列出来前台游客视角注册登录密码加密存储首页展示热门景点和推荐线路景点、线路、酒店三个列表关键词搜索、分类筛选、分页详情页图片、介绍、价格、余量、用户评论下单购买门票、线路、酒店都能下单订单能取消个人中心我的订单、订单评价、个人资料后台管理员视角景点管理新增、修改、上下架、删除逻辑删除线路管理线路天数、包含景点、出发日期酒店管理房型、价格、房量订单管理查看所有订单、修改订单状态、退款标记用户与评论管理禁用用户、隐藏不当评论、简单统计这套范围做出来大概2000多行核心代码对毕设来说刚刚好。千万别再加什么优惠券、秒杀、直播带货每加一个都会让答辩前的你更狼狈。2.2 users、attractions、routes、hotels、orders字段细节核心就五张表加一张评论表。下面是字段设计里最容易踩坑的几个点表名关键字段设计备注usersid, username, password, nickname, avatar, phone, status, create_timepassword存BCrypt密文status控制禁用attractionsid, name, location, image, description, price, stock, hot, statusprice用decimal(10,2)stock表示可售门票数routesid, name, days, scenic_list, price, max_count, travel_date, descriptionscenic_list存景点id逗号串毕设里够用hotelsid, name, address, star, room_type, price, room_count, imagestar表示星级不是数据库主键ordersid, order_no, user_id, item_type, item_id, quantity, total_amount, status, create_time, pay_time, cancel_timeitem_type区分1景点/2线路/3酒店commentsid, user_id, item_type, item_id, content, rating, create_time, statusrating取值1-5status控制是否展示MyBatis-Plus里建议统一加create_time和update_time字段用MetaObjectHandler做自动填充这样插入更新不用手动set时间。所有表主键都用Long自增别在毕设里去玩雪花算法。delete操作一律用逻辑删除字段is_deleted这样后台数据统计不会被物理删除干扰答辩时还能顺口说一句我用了逻辑删除保护业务数据这就是一个小亮点。2.3 订单状态机与下单幂等性订单是整站的业务核心状态不要瞎存字符串用数字常量status含义可流向0待支付1已支付、2已取消1已支付3已完成、2已取消退款场景2已取消终态3已完成终态所有状态流转的SQL都要带条件判断。举个例子取消订单UPDATE orders SET status 2, cancel_time NOW() WHERE id #{orderId} AND user_id #{userId} AND status 0如果更新影响行数是0说明订单不是待支付状态拒绝本次操作。这样即使前端按钮被快速点击两次或者后端收到重复请求也不会把同一单取消两遍。订单号生成我用的是yyyyMMddHHmmss 4位随机数 用户ID后四位在做演示数据时看起来像正经订单号又足够唯一。真要防并发幂等的话给order_no建唯一索引插入报DuplicateKey就说明重复下单直接返回请勿重复提交。3. 核心功能的开发节奏登录、搜索、下单、评价3.1 注册登录与登录拦截器注册时密码一定要加密。我用的是spring-security-crypto里的BCryptPasswordEncoder单独引这个依赖就行不需要整个Spring Security。注册流程里做两次校验第一次是Controller参数校验用户名非空、密码长度、手机号格式第二次是Service里查重。注意校验逻辑放在Service里去查数据库Controller里只做格式校验。登录成功后把用户信息放进Session同时写一个简单登录拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(user) ! null) { return true; } response.sendRedirect(/login); return false; } }然后注册到WebMvcConfigurer里放行登录注册页和静态资源registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/, /login, /register, /css/**, /js/**, /images/**, /upload/**);这么做之后所有需要登录的页面都会有拦截保护。老师如果问你权限控制怎么做你能答上来登录态校验角色判断比空写个过滤器强得多。后台管理再单独加一个管理员判断在Controller里检查session里的role字段或者写一个AdminInterceptor粒度不用太细。3.2 关键词搜索、菜单分类与分页查询景点列表最常规的写法是QueryWrapperAttraction wrapper new QueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(name, keyword) .or().like(location, keyword); } wrapper.eq(status, 1).orderByDesc(hot);注意or()和eq()之间的括号问题如果不加条件拼接很容易把status条件也用or并联导致筛选失效。稳妥写法是先构造好wrapper再交给service或者直接手写XML里的动态SQL。分页用MyBatis-Plus的PaginationInnerInterceptor在config里注入Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }之后调用new Page(pageNum, pageSize)接口返回IPage对象前端取records、total、pages。这对纯单表分页完全够用。景点详情页需要连表带评论一起统计时我会写一个自定义Mapper方法用Page当第一个参数让拦截器帮我们生成limit和count具体写法放在后面踩坑章节讲因为这一块最容易出事。3.3 下单的事务与余票防超卖下单逻辑我拆成三步查商品、扣余量、插订单包在一个事务方法里。扣余量不能用先查再set要靠条件更新保证原子性int rows attractionMapper.updateStock(id, 1); // update stock stock - 1 where id ? and stock 0 if (rows 0) { throw new BusinessException(余票不足请选择其他日期); }只有这条SQL执行成功才继续插入订单记录。订单插入完成后返回订单号让前端跳到下单成功页。整个方法标上Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderParam param) { ... }rollbackFor一定要写因为Spring默认只回滚RuntimeException万一中间抛个SQLException或者自己包裹后的异常事务会悄悄失效。线路和酒店的下单逻辑完全一样只是表名不同。支付那一段我不建议真的接支付宝或微信演示时做一个去支付按钮模拟支付成功把status从0改成1就行。答辩老师都清楚毕设支付都是模拟不会为难你。3.4 评论评分与后台管理闭环评论功能要写两个闭环游客下单后可以评价后台可以审核隐藏。游客评价时Controller里从session拿userId评论表里存item_type和item_id不直接存景点名展示时再去关联查。评分用1到5的整数radio存数据库后用SQL聚合出平均分SELECT ROUND(AVG(rating), 1), COUNT(*) FROM comments WHERE item_type 1 AND item_id #{id} AND status 1景点详情页展示评价列表时注意用Thymeleaf的th:text而不是th:utext输出评论内容防止用户把脚本塞进评论里形成XSS攻击。这个点我在第4章还会展开因为很多同学的毕设都栽在富文本和评论防XSS上。后台管理就是一层crud循环景点、线路、酒店三个Controller加上一个分页列表页面用同一个后台模板改字段就行。订单管理页按status用下拉筛选能改状态、能模拟退款。数据统计我做了最简单的三个数字总订单数、已完成订单数、总收入用一个SELECT SUM(total_amount) FROM orders WHERE status 1搞定够展示用。想看曲线的可以每月日期分组GROUP BY DATE_FORMAT(create_time, %Y-%m)两行SQL的事。4. 一路踩坑的真实排查链路序列化、上传、分页与事务4.1 LocalDateTime序列化格式错乱症状前端页面或者接口里返回的create_time变成了一串数组[2024,5,1,10,30,0]或者显示成一长串数字。很多同学第一反应是去yml里配spring.jackson.date-format但配了半天没用因为那个配置作用于java.util.Date对Java 8的LocalDateTime无效。正确的两个做法字段上直接加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime createTime;或者在配置类里全局设置spring: jackson: serialization: write-dates-as-timestamps: false加上jackson的jsr310依赖Spring Boot一般已自动引入LocalDateTime就会序列化成ISO标准格式。我自己的习惯是两种都做字段注解兜底全局关掉时间戳序列化保证所有接口时间格式统一。至于Thymeleaf页面上要显示时间用#temporals.format(createTime, yyyy-MM-dd HH:mm:ss)别直接用toString。4.2 图片上传到本地却刷新404这是我见过最普遍的怪问题。前台添加上传接口自己测的时候图片明明传上去了地址也对但是复制到浏览器却404。原因通常是这么几种第一MultipartFile默认单文件最大1MB传大一点的景点图直接被拒。配置里加上spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB第二上传目录写了项目运行目录下的相对路径比如/upload/在IDEA里跑没事因为路径是IDEA的临时工作目录一旦打成jar在Linux上启动相对路径变得不可捉摸图片要么没写进去要么找不到。踩过几次这种坑后我彻底改成外部绝对路径application.yml里配file.upload-path/data/travel/upload/上传代码把MultipartFile保存到这个目录然后数据库里只存相对URL比如/upload/20240912/xxx.jpg最后在WebMvcConfigurer里把/upload/**这个URL前缀映射到文件目录去。Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); }改完之后无论本地还是服务器页面都能稳定显示图片。记住文件一定落到外部目录绝对路径统一配在配置文件里URL前缀和磁盘路径做映射别把上传逻辑和Tomcat工作目录耦合。4.3 MyBatis-Plus自定义SQL分页统计出错做景点详情连评论、线路包含景点这类多表查询时免不了写自定义SQL。你如果直接在Select注解里写了SELECT ... LIMIT #{offset}, #{size}然后又把Page对象传进去分页拦截器会再补一条LIMIT结果SQL里出现两个LIMIT直接报错。正确的姿势是自定义SQL里不写LIMIT只写业务查询把PageT作为Mapper方法第一个参数传进去PaginationInnerInterceptor会自动在查询后面加LIMIT同时生成一个COUNT查询用于统计总条数。但多表联查时自动生成的COUNT SQL经常统计不准比如LEFT JOIN导致重复行数变大。我最后的处理方式是自己控制计数SQLSELECT COUNT(*) FROM ( SELECT a.id, a.name, COUNT(c.id) AS comment_count FROM attractions a LEFT JOIN comments c ON c.item_type 1 AND c.item_id a.id AND c.status 1 WHERE a.status 1 if testkeyword ! null and keyword ! AND (a.name LIKE CONCAT(%, #{keyword}, %) OR a.location LIKE CONCAT(%, #{keyword}, %)) /if GROUP BY a.id ) t分页的主查询对应写成查明细的SQL注意GROUP BY字段不要出现在SELECT里乱七八糟的别名上否则MySQL ONLY_FULL_GROUP_BY模式下直接报错。这一块建议基础一般的同学直接用单表分页Service里补评论数能少一半麻烦。4.4 下单事务静默失效的典型现场有同学发来代码问我下单方法明明加了Transactional但故意在插入订单后抛异常发现余量照样扣了。我一看代码他是在同一个Service类的另一个方法里调用了加了事务注解的方法比如controller调saveOrdersaveOrder内部this.updateStock(...)。Spring的事务是基于AOP代理的内部自调用相当于直接穿透代理注解根本没生效。解决的办法有三种任选一个都行把要事务包裹的下单逻辑单独拆一个Service实现类Controller调用这个Service的public方法事务代理才会介入。在类里注入自己的代理对象用代理对象调用。在方法内部用AopContext.currentProxy()强转后调用需要设置exposeProxy。我自己习惯用第一种结构最清楚。另外还要检查调用链上有没有try-catch把异常吞掉。事务只有在你把异常抛出去的时候才能感知你在方法内部catch住又return了个错误提示Spring能怎么办它只会觉得自己很成功。所以下单方法里所有的业务异常都直接throw让全局异常处理去统一提示不要return个false回来。还有一点很少人注意MySQL要是用了MyISAM这种不支持事务的引擎Transactional写了也白写5.7默认就是InnoDB所以一般没问题但导入老师给的模板库时很可能碰上旧表引擎建表SQL里统一ENGINEInnoDB最稳。5. 部署上线与毕业答辩的加分细节5.1 配置文件分环境一处打包到处运行我在application.yml最开头只写公共配置然后拆出application-dev.yml和application-prod.ymldev给本地连127.0.0.1的库prod给服务器连远端数据库。启动时通过参数切环境java -jar travel-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod数据库密码、上传路径这类环境相关配置只放到环境配置文件里不要写死在公共yml。好处是本地开发用dev答辩前部署到服务器用prod同一个jar包切个参数就行也方便老师在你电脑上直接跑。配置文件里顺手把端口配成8080然后打包时用mvn clean package -DskipTests生成target目录下的jar。5.2 Linux服务器部署与Nginx反代整个部署流程其实半小时能走完# 1. 上传jar包和SQL文件 scp travel.jar root服务器IP:/opt/travel/ # 2. 服务器上创建上传目录并授权 mkdir -p /data/travel/upload chmod -R 755 /data/travel/upload # 3. 数据库导入SQL mysql -uroot -p travel_db travel.sql # 4. 启动服务 nohup java -Xms256m -Xmx512m -jar travel.jar \ --spring.profiles.activeprod \ /opt/travel/travel.log 21 如果想让网站访问更正经一点可以用Nginx做反向代理。配置一个server块把域名或公网IP的80端口代理到127.0.0.1:8080同时设置client_max_body_size 20m处理大图上传请求头里加上X-Forwarded-For让Spring拿到真实IP。真到了答辩演示那天老师如果问你的系统能远程访问吗你说我已经部署在云服务器上了然后把手机流量打开访问给他看这一下印象分基本就稳了。5.3 演示数据准备与答辩高频提问清单我见过很多同学系统做完上答辩台却因为没有演示数据而冷场。景点只有一条测试评论也没有订单列表空荡荡。演示前一定要自己把数据填满至少8个景点覆盖不同分类和热门度2到3条线路3个酒店然后用两个测试用户分别下单到不同状态留下几条真实感评论。演示前把浏览器缓存清掉从注册或登录开始一步一步走完搜索景点→看详情→下单→我的订单→评价这条链中途不用快进节奏反而稳。下面是答辩环节被问概率最高的几个问题直接背答案没问题Spring Boot为什么能自动装配核心是SpringBootApplication里的EnableAutoConfigurationSpring Boot的starter包里的META-INF/spring.factories或AutoConfiguration.imports里声明了大量自动配置类再配合Conditional系列注解按条件生效项目启动时把需要的Bean自动注册进容器。分页是怎么实现的MyBatis-Plus的分页插件是一个Mybatis拦截器拦截带Page参数的Mapper方法在执行前改写SQL补上LIMIT语句并额外执行COUNT查询拿到总记录数。怎么防止用户把库存买超核心是数据库条件更新update attraction set stockstock-1 where id? and stock0影响行数为0就说明没库存了加上事务保证扣库存和插订单要么都成功要么都失败。为什么不接真实支付毕设重点是业务流程闭环支付作为第三方服务可通过模拟接口展示订单状态流转真实对接支付宝/微信涉及资质和密钥不适合个人毕设环境。一节一节写下来你会发现其实每个功能拆开都不是什么黑科技但把它们串成一个能跑、能讲、能部署的系统才是毕设真正的意义。我最后唯一想强调的还是那句话不要为了显得厉害去堆一堆自己讲不清的技术栈答辩老师最想看到的是一个学生能把自己做的每一行代码说明白而不是一个连自己项目都圆不回来的高级工程师假象。把这套旅游网站做扎实从数据库到部署全自己走一遍你毕业答辩那天会比大多数人从容得多。
返回列表