选旅游管理系统做毕设,大多数人是图它业务不复杂、模块好划分、演示也直观。再加上 Spring Boot 这个技术栈在 Java 方向里的普及程度,这道题算是把“做出成果”和“答辩能讲”之间的平衡拿捏得比较稳的。这篇内容我就按项目拆解的思路来写:从设计定位、数据库表结构、核心代码实现,到常见坑和答辩高频问题,一次性讲透。无论你是刚拿到源码还没跑通,还是打算从零开始写一套,都能直接参考这套思路。
1. 项目定位:为什么旅游管理系统是毕设的“稳妥牌”
1.1 业务场景清楚,模块划分天然占优势
很多毕设题目看起来高大上,但一落地就发现需求边界模糊,越做越不知道功能该画到哪。旅游管理系统没有这个问题。它的核心业务路径非常明确:用户注册登录、浏览景点和线路、查看详情、下单支付、发表评论、收藏点赞,后台管理员则负责内容维护、订单处理和用户管理。
这套业务天然分成了两端。前端面向游客,强调的是信息展示的清晰度和下单流程的顺畅度;后端面向管理员,强调的是数据维护的效率和订单状态的准确性。你可以在此基础上再扩展公告管理、轮播图管理、数据分析看板,但基础的骨架不会变。对于毕设来说,这恰恰是最好把控的地方——需求清楚了,代码结构就不会散。
我见过不少同学答辩时被问“你这个系统解决了什么问题”,如果选的是旅游管理,回答起来就很自然。你可以直接说:围绕旅游信息分散、预订流程不透明这两个痛点,做一个集中展示和管理的平台。这个回答基本挑不出毛病。
1.2 技术栈覆盖面平衡,演示成本相对低
旅游管理系统不挑技术栈。你可以只用 SSM 做传统分层,也可以用 Spring Boot + MyBatis Plus 做经典组合,还能用前后端分离方案把 Vue 拉进来。从近几年毕设的普遍情况看,Spring Boot + Vue 前后端分离是最主流的配置。
选择 Spring Boot 的理由很实在。它对新手友好,内嵌 Tomcat,减少了很多配置上的痛苦;自动装配和 starter 机制让项目搭起来非常快;配合 MyBatis Plus,单表 CRUD 甚至不用写 SQL。这样你的精力可以集中在业务逻辑上,而不是被 XML 映射文件和各种 bean 配置劝退。
技术栈选对了,整个项目的产出效率是质的提升。同样是两周开发时间,别人可能还在调 Spring 的 XML 配置,你已经把订单流程跑通了。这也是为什么我强烈建议毕设项目优先选 Spring Boot 生态。后面我会具体列一份选型清单,并说明每一项选择的理由。
2. 整体设计与技术选型:从功能模块到依赖清单
2.1 前台与后台的模块拆解
旅游管理系统的功能模块可以画成两条线:用户端和管理端。
用户端包括:
- 用户注册登录,登录后修改个人信息、查看订单和收藏
- 首页展示轮播图、热门景点、推荐线路
- 景点和线路列表页,支持分类筛选、关键词搜索、分页浏览
- 景点或者线路详情页,展示图片、价格、介绍、评价,支持加入收藏和直接下单
- 订单模块,用户下单、支付(模拟)、取消订单、查看订单状态
- 评论模块,对已消费的景点或线路发表评价,并打分
管理端包括:
- 管理员登录,一般是预设账号,不走注册流程
- 景点和线路的增删改查,图片上传、上下架
- 分类管理、轮播图管理、公告发布
- 订单管理,查看所有订单、修改订单状态
- 用户管理,查看用户列表、禁用或启用账号
- 评论管理,删除不当评论
这套模块覆盖了一个信息管理系统的完整闭环。从数据库设计来说,实体之间的关联关系很清晰:用户与订单一对多,用户与收藏一对多,景点与评论一对多,订单与商品多态关联。只要你把表设计好,后面写代码就是按部就班的事。
2.2 技术栈清单与选型理由
下面这个技术栈是一个比较稳的搭配,适合大多数以 Spring Boot 为基础的毕设项目。
| 层次 | 技术选型 | 选型理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定、资料多、与 JDK 8 搭配最省心 |
| ORM 层 | MyBatis Plus 3.5.x | 单表 CRUD 不写 SQL,分页插件好用 |
| 数据库 | MySQL 8.0 | 免费、主流、安装调试教程多 |
| 权限控制 | JWT + Spring Boot 拦截器 | 无状态鉴权,前后端分离演示效果好 |
| 缓存 | Redis(可选但推荐) | 缓存景点热门数据,答辩加分项 |
| 前端 | Vue 3 + Element Plus + Vite | 组件化开发速度快,界面更现代化 |
| 接口调用 | Axios | 前后端分离标配 |
| 项目构建 | Maven | 依赖管理和打包都靠它,课程里也常用 |
| 工具类 | Hutool | JWT 生成、加密校验都封装好了 |
这个组合里,最值得解释的是为什么用 MyBatis Plus 而不是传统 MyBatis。对毕设来说,你的表多、关联查询有但不是特别复杂,如果用原生 MyBatis,光是写每个实体的 resultMap 就够耗时间的。MyBatis Plus 把单表操作直接封装好了,你只需要写一对多查询、多表关联这类相对复杂的 SQL。这符合“把时间花在核心逻辑上”的原则。
JWT 和拦截器的组合也要说明一下。很多教程一上来就上 Spring Security + JWT,这确实是最标准的方案,但 Spring Security 的过滤器链对新手来说太劝退了。毕设不是企业生产环境,你要的是把前端传回来的 token 校验一下、放行还是拦截,这个过程用拦截器加 JWT 工具类就够。等以后工作接触生产项目,再切换成 Spring Security 也没问题。
2.3 功能亮点的延展思考
如果你的项目想从“普通版本”升级成“有点亮点”的版本,可以在上面模块基础上做三个方向的延展。
第一个方向是数据可视化。管理员后台加一个数据统计页,用 ECharts 展示每周订单量变化、最受欢迎的景点 Top10。这个功能代码量不大,但视觉效果很好,答辩时展示出来很加分。
第二个方向是订单超时自动取消。用 Spring 的 @Scheduled 定时任务,定时扫描超时未支付订单并自动关闭。这涉及一点任务调度的知识,是可以展开讲的技术点。
第三个方向是引入 Redis 缓存。景点详情、首页推荐列表这些读多写少的数据,可以缓存到 Redis,降低数据库压力。答辩老师问到性能优化时,这就是一个扎实的回答。
我个人的建议是:基础功能做扎实之后,再挑其中一个延展方向做。不要把每个方向都加上,否则项目的体量会失控,开发周期也容易拖。
3. 数据库设计:旅游管理系统的表结构核心
3.1 核心表清单与字段说明
数据库是整个项目的底盘。我见过很多同学后端的代码结构都写完了,结果表设计不合理,关联查询写得特别别扭。旅游管理系统的表可以按业务线来梳理,核心就是下面这几张。
| 表名 | 作用 | 关键字段说明 |
|---|---|---|
| user | 用户表 | id、username、password、phone、nickname、avatar、role、status |
| scenic_spot | 景点表 | id、name、cover_url、province、city、address、price、open_time、description、view_count |
| travel_line | 线路表 | id、title、days、price、start_city、end_city、cover_url、description |
| category | 分类表 | id、name、sort |
| orders | 订单表 | id、order_no、user_id、product_type、product_id、total_price、status、create_time、pay_time |
| comment | 评论表 | id、user_id、product_type、product_id、content、score、create_time |
| favorite | 收藏表 | id、user_id、product_type、product_id、create_time |
| banner | 轮播图表 | id、image_url、target_url、sort、status |
这里有一个容易被忽略的坑:订单表不要命名为 order。order 在 MySQL 里是排序关键字,如果你直接用 order 当表名,执行 SQL 时会报语法错误。很多人第一次跑建表脚本的时候就在这里栽了跟头。用 orders 是为了规避关键字冲突,这算是数据库设计里的一个小经验。
评论表和收藏表都用到了 product_type 这个字段,用来区分评论或收藏的对象是景点还是线路。这是一种多态设计的思路,避免了为景点和线路分别建一套评论表和收藏表。查询的时候按 product_type + product_id 过滤就行,实现简单,逻辑也清晰。
3.2 订单状态设计与状态流转
订单状态是管理系统的核心逻辑之一,千万不要把状态字段设计成随便填写的字符串。更好的做法是用数字枚举:0 表示待支付,1 表示已支付,2 表示已取消,3 表示已完成,4 表示退款中。这样设计有几个好处:存储空间小,查询性能好,并且可以在代码里用枚举类统一管理。
订单状态的流转要形成闭环。用户下单后状态为 0,支付成功后状态变为 1,管理员完成服务后状态变为 3,用户申请退款后状态变为 4。取消订单的情况分两种:用户在支付前手动取消,或者超时未支付被定时任务自动置为 2。这个流转关系一定要清楚,写代码的时候才不会出现“从已支付直接跳转到已取消”这种不合逻辑的情况。
订单号字段也需要单独说。不要用自增主键直接当订单号展示给用户,这样既容易暴露数据量,也不够专业。可以在生成订单时拼接时间戳、用户 ID 和随机数生成唯一的订单号,比如用 Hutool 的 IdUtil 工具类创建带前缀的雪花 ID。
3.3 建表 SQL 示例
下面的建表语句是景点表和订单表的参考写法。字段类型和长度要根据实际情况调整,但整体结构可以直接用。
CREATE TABLE `scenic_spot` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '景点ID', `name` varchar(100) NOT NULL COMMENT '景点名称', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图', `province` varchar(50) DEFAULT NULL COMMENT '所属省份', `city` varchar(50) DEFAULT NULL COMMENT '所属城市', `address` varchar(255) DEFAULT NULL COMMENT '详细地址', `price` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '门票价格', `open_time` varchar(50) DEFAULT NULL COMMENT '开放时间', `description` text COMMENT '景点介绍', `view_count` int NOT NULL DEFAULT 0 COMMENT '浏览次数', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态 1上架 0下架', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='景点表'; CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` varchar(64) NOT NULL COMMENT '订单编号', `user_id` bigint NOT NULL COMMENT '用户ID', `product_type` tinyint NOT NULL COMMENT '商品类型 1景点 2线路', `product_id` bigint NOT NULL COMMENT '商品ID', `total_price` decimal(10,2) NOT NULL COMMENT '订单金额', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态 0待支付 1已支付 2已取消 3已完成 4退款中', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里有两个细节值得记住。第一个是 id 用 bigint 而不用 int,第二是时间字段统一用 datetime 并在建表时指定默认值。这些规范看起来不起眼,但直接影响系统以后的扩展和维护。
3.4 查询索引的设计思路
索引不需要一开始建很多,但有两类字段值得加索引。第一类是外键关联字段,比如 orders 表的 user_id、product_type + product_id;第二类是高频查询字段,比如景点表的 city、status。这些字段在列表页筛选和详情查询里经常用到,加了索引之后,数据量增长到几万条时查询速度也不会有明显下滑。
索引也要克制,不要每个字段都建。毕设阶段数据量不大,索引过多的代价是写入变慢,而且会给人“为了用索引而用索引”的感觉。我在实际项目里往往只保证订单表、评论表和收藏表的关联字段有索引,其他表看情况处理。这样足够应付答辩的提问了。
4. 从零搭建项目:实操步骤全记录
4.1 初始化环境和项目骨架
开始写代码之前,先把环境准备好。比较稳的组合是 JDK 8 + Maven 3.6+ + MySQL 8.0 + IDEA。JDK 8 是很多毕设机器的常见版本,搭配 Spring Boot 2.7.x 不需要额外处理模块化相关的问题,踩坑最少。
项目骨架建议直接通过 Spring Initializr 生成。地址就是 start.spring.io,选版本、填 group 和 artifact,依赖勾选 Spring Web、Validation、MySQL Driver、Lombok。生成之后导入 IDEA,再用我下面的方式补充 MyBatis Plus 和 JWT 相关依赖。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency>MyBatis Plus 用的是 3.5.x 版本,这个版本对 Spring Boot 2.x 的兼容性是最好的,分页插件和代码生成器也都很稳定。Hutool 主要拿来生成 JWT、做 MD5 加密,少写很多工具类代码。
生成好项目之后,先用一个小 Demo 确认能启动起来,再开始加业务代码。我见过很多同学一上来就写一大堆,结果连 Spring 容器都没起来,排查问题的时候根本分不清是环境问题还是代码问题。
4.2 核心配置文件与通用组件
项目的 application.yml 里需要配置数据源、MyBatis Plus 和 JWT 相关参数。下面是我常用的写法。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key-please-change expire-hours: 72说几个配置上的注意点。url 里的 serverTimezone=Asia/Shanghai 必须要有,不然 MySQL 8.0 会报时区错误。map-underscore-to-camel-case 开启后,数据库里的 create_time 就能自动映射成 createTime,少写很多映射配置。逻辑删除的配置可以让“删除”变成“update 置位”,避免物理删除导致的数据丢失,这也是一个可以拿出来讲的细节。
通用组件方面,我建议先写三个类。第一个是统一返回体 Result,第二个是全局异常处理器,第三个是分页响应的封装。统一返回体的作用是约定一个标准格式,前端不管是成功还是失败都按同一个结构解析,联调的时候会省很多事。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理器用一个 @RestControllerAdvice 注解就能实现,统一捕获业务异常和兜底异常,返回 Result 格式的错误信息。这个组件写完之后,业务代码里就只管抛异常,不用在每层手动 try-catch。
4.3 登录鉴权:JWT + 拦截器解析
登录鉴权是毕设里最容易出问题的地方。我的推荐实现顺序是这样的:用户输入账号密码,后端校验通过后用 Hutool JWTUtil 生成 token 返回前端;前端把 token 存到本地,并在每次请求的请求头里带上 Authorization 字段;后端写一个拦截器,在请求进入 Controller 之前校验 token,校验通过就把用户信息放进请求上下文。
拦截器的核心逻辑可以参考下面的代码。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StrUtil.isBlank(token)) { throw new BusinessException("未登录,请先登录"); } try { JWT jwt = JWTUtil.parseToken(token); jwt.setCharset(StandardCharsets.UTF_8); JWTValidator.of(jwt).validateDate(new Date()); Long userId = Long.valueOf(jwt.getPayload("userId").toString()); request.setAttribute("userId", userId); return true; } catch (Exception e) { throw new BusinessException("登录已过期,请重新登录"); } } }注册拦截器的时候,要注意放行哪些路径。登录接口、注册接口、景点列表和详情这些公开接口必须放行,否则前端还没登录,首页的数据都拿不到。管理端的路径可以统一加 /admin/ 前缀统一拦截,这样逻辑更清晰。
这个方案的优点是无状态。后端不需要维护 Session,token 自带了用户身份信息。缺点是 token 签发后无法主动失效,除非你把 token 存到 Redis 里再做一层校验。毕设阶段我建议用无状态的 JWT 就够,如果答辩老师追问“用户修改密码之后老的 token 还有效怎么办”,你再用 Redis 黑名单方案补充回答,效果反而更好。
4.4 分页与参数校验
旅游管理系统的列表页全部要分页。MyBatis Plus 使用分页需要先配置分页插件,这是一个必要的步骤,不配置的话 Page 对象不会生效。
分页配置的代码非常简单,核心是向 MybatisPlusInterceptor 里添加 PaginationInnerInterceptor,指定数据库类型为 MySQL。配置好之后,在 Service 层调用 Page 对象查询,返回的数据里自动带有总条数和当前页列表。前端用 Element Plus 的 Pagination 组件对接就很顺手。
参数校验方面,统一用 Spring Boot 自带的 validation 依赖。在实体字段上加 @NotBlank、@NotNull、@Size 这些注解,Controller 入参用 @Valid 触发校验,再配合全局异常处理器把校验失败的提示返回给前端。这套流程写起来不复杂,但会让项目看起来比大多数毕设规范很多。
5. 核心模块实现:从搜索到下单的业务闭环
5.1 景点与线路的搜索分页实现
景点列表页是前台访问量最高的接口。它的核心需求是支持按关键词搜索、按分类筛选、按城市筛选,并返回分页数据。使用 MyBatis Plus 的 LambdaQueryWrapper 可以很方便地拼接这些条件。
public Page<ScenicSpot> queryScenicSpotPage(int page, int size, String keyword, String city) { Page<ScenicSpot> pageInfo = new Page<>(page, size); LambdaQueryWrapper<ScenicSpot> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ScenicSpot::getStatus, 1); if (StrUtil.isNotBlank(keyword)) { wrapper.like(ScenicSpot::getName, keyword) .or().like(ScenicSpot::getDescription, keyword); } if (StrUtil.isNotBlank(city)) { wrapper.eq(ScenicSpot::getCity, city); } wrapper.orderByDesc(ScenicSpot::getViewCount); return scenicSpotMapper.selectPage(pageInfo, wrapper); }这段代码里有几个值得注意的细节。状态为 1 的查询条件是写死的,因为下架的景点不应该出现在用户端。排序用 viewCount 倒序,可以做一个“热门景点”的效果。关键词搜索同时匹配名称和描述,这样用户搜“西湖”能出来,搜“山水”也能出来相关的结果。
线路表的搜索和景点基本同构,如果有时间,可以把公共的查询逻辑抽取出来,但毕设阶段不做抽象也完全可以。写清楚比过度设计更重要。
5.2 下单流程与库存处理
下单是整个系统中业务逻辑最密集的接口。流程是:前端提交商品类型、商品 ID、购买数量,后端查询商品信息、计算价格、生成订单号、保存订单,并返回一个模拟支付的凭证。
这里要注意一个死循环问题。很多同学会把下单和“立即支付”写在一起,导致用户下单后必须马上支付,跳出了支付页面订单就无法继续。更合理的做法是把下单和支付分成两个阶段。下单单单负责创建待支付订单并扣减库存,支付接口负责把订单状态从待支付改成已支付。这样才能接上定时任务处理超时订单的逻辑。
库存扣减这段是答辩老师比较喜欢问的地方。一个简单的防超卖写法是使用乐观锁更新,SQL 类似于 update product set stock = stock - 1 where id = ? and stock > 0。更新影响的行数为 1 表示扣减成功,为 0 则表示库存不足。这种写法在毕设的并发规模下已经足够可靠,还能体现你理解“先检查再更新”的并发隐患。
“库存不足”的异常要在 Service 层抛出,并通过全局异常处理器返回给前端。不要直接在 Controller 里回一个 error 状态,这样会造成成功状态和错误状态混在一起,前端处理起来很痛苦。
5.3 评论与收藏功能的实现思路
评论和收藏是两个比较典型的子模块,逻辑简单但能体现系统的完整度。
评论功能的实现分为两步。提交评论时,用户要传商品类型、商品 ID、评分和评论内容。后台保存评论之前,可以校验该用户是否已完成过这个商品的订单,防止“没有去过就瞎评论”,这个校验逻辑虽然只有几行代码,但能体现出你考虑业务的细致度。评论列表在商品详情页按时间倒序展示。
收藏功能可以用一张 favorite 表记录用户和商品的关系。重复收藏的问题要处理掉——用户第一次点击可以收藏,再点应该是取消收藏,而不是新增一条重复记录。查询时可以通过 count(user_id + product_type + product_id) 是否大于 0 来判断当前用户是否已经收藏过这个商品。
用户在个人中心里需要能看到“我收藏的列表”。这个接口要把收藏表和商品表关联起来,示例代码的思路是:先用收藏表查出 product_type 和 product_id 列表,再根据商品类型分别去景点表和线路表查出详情。这样做虽然多了几次查询,但结构清晰,也容易理解。
5.4 后台管理端的关键设计
后台管理端的设计重点不是复杂的业务逻辑,而是权限控制和操作效率。管理员的接口要统一加上身份校验,拦截器在解析 token 时顺带判断用户角色是否为管理员,不是则直接拒绝。这个判断逻辑放在拦截器里是最简单的,因为所有 /admin/** 的请求都在同一层拦截。
后台的 CRUD 操作可以做得比较高效,因为 MyBatis Plus 已经把常用的增删改查封装好了。新增或修改景点时,需要注意图片上传的处理。图片不要存成二进制进数据库,那会让数据库膨胀且性能很差。最常用的方案是上传到服务器本地指定目录,然后返回一个访问路径,数据库里只存路径字符串。如果想省事,也可以用 Base64 存,但我不推荐这么做,后期数据量上来会很吃亏。
订单管理页面要支持按状态筛选。管理员主要关注待支付、已支付和退款中的订单。修改订单状态时,要注意“接口没有做幂等控制”的问题,比如退款按钮被重复点击可能导致状态被反复修改。一个简单的做法是,执行修改前先判断当前状态是否符合预期,符合才允许修改。
6. 常见问题与踩坑实录,附答辩准备思路
6.1 环境与配置类问题排查
我整理了毕设项目里出现频率非常高的几类问题,按环境配置、代码逻辑、联调三个维度来说。
| 问题描述 | 可能原因 | 解决思路 |
|---|---|---|
| 启动时报数据库连接失败 | 数据库名错误、密码错误、时区配置缺失 | 检查 url、username、password,补上 serverTimezone=Asia/Shanghai |
| 8080 端口被占用 | 本地有别的服务占用端口 | 换个端口,或者找到占用进程结束掉 |
| 启动时报 Bean 找不到 | Service 或 Mapper 的包扫描路径不对 | 检查启动类上的 @SpringBootApplication / @MapperScan 路径 |
| 分页不生效,查出来是全量数据 | 没有配置分页插件 | 配置 PaginationInnerInterceptor 到 MybatisPlusInterceptor |
| 前端请求接口报 401/未登录 | token 没传或过期 | 检查前端请求拦截器是否带上 Authorization 请求头 |
| 返回的时间格式不正确 | 时间序列化时区问题 | 在 yml 里配置 spring.jackson.time-zone=GMT+8 |
这里面最值得展开的是分页不生效的问题。很多人以为只要 new Page() 就能分页,但 MyBatis Plus 必须显式注册分页插件。配置代码我前面给过,它必须放到 MybatisPlusInterceptor 里面,顺序不能错。还有一类常见问题是实体类的 @TableName 注解写错表名导致查询报错,这个在第一次联调时也容易碰到。
6.2 业务逻辑里容易被问住的细节
答辩老师不一定在意你代码写得多么花哨,但他肯定会在意你对自己系统逻辑的自洽程度。有几个细节最好提前想清楚。
第一,为什么订单超时要取消?这个问题是从“保证库存释放”的角度切入的。用户下单后占用了库存但不支付,如果不取消,其他用户就买不了票。定时任务的实现原理要能讲出来:使用 @Scheduled 注解,每 30 秒扫一次状态为待支付且创建时间超过 15 分钟的订单,把状态改成已取消并归还库存。
第二,为什么评论要用 product_type 区分对象,而不是分别建两张表?这个问题的核心是“多态关联设计的权衡”。用公共字段可以减少表数量、减少重复的接口,代价是无法直接用外键做数据库级联。在毕设的数据量下,应用层判断完全足够。
第三,为什么不用 Session 而用 JWT?这个问题的核心是“前后端分离场景下无状态鉴权的优势”。Session 依赖服务端存储,接口跨域或集群部署时会有共享 Session 的麻烦。JWT 把用户状态存在 token 里,后端解析就能得到用户信息,更符合前后端分离架构的调用方式。
第四,数据库查询慢怎么优化?这个问题的核心是“索引和缓存”。先从 SQL 入手,检查 where 条件字段有没有索引;再从业务场入入手,景点详情这类读多写少的数据可以用 Redis 缓存。这两个点讲清楚,你的项目在性能上就不虚。
6.3 演示时间的分配经验
答辩现场最怕的是把几分钟的演示时间全花在“登录、点一个列表、再点一个详情”上。老师还没看到核心功能,时间就到了。我建议的演示顺序是:快速登录进系统,30 秒内展示首页轮播和热门推荐;重点演示一个景点的详情和下单流程,展示到订单状态变化;切到后台,展示订单管理和商品上下架的操作。这样一圈下来,前台用户流程和后台管理流程都覆盖到了。
还有一个小技巧,提前准备一份干净的测试数据,包括合理的景点信息、一笔待支付订单、一笔已支付订单。演示到订单模块时直接用这些数据,不要现场现造。数据太乱或者列表为空,都会让人感觉这个系统还没有真正用过。
6.4 从源码到真正上手的建议
如果你拿到的源码已经是完整的项目,不要急着直接启动。先把项目结构过一遍,理清 Controller、Service、Mapper 的分层关系,找到登录接口、景点列表接口和订单接口分别在哪个类里。然后用 Postman 或者前端的接口请求看一遍核心流程,画一张数据流向图放在自己的笔记里。这样即使答辩时不看代码,你也说得清每个功能背后的实现路径。
我见过不少同学囤了很多份源码,但最后能讲明白的只有自己动手整理过的那一份。源码只是个起点,真正常握住的是理解项目逻辑的能力。你愿意花时间把表结构、核心接口、状态流转都梳理一遍,答辩老师问什么你都能接住。这也是我认为做毕设最有价值的地方——不是交差,而是真正弄懂一个系统是怎么跑起来的。