之所以在2026年看到"基于Spring Boot的智慧旅游系统"这类课题还愿意接,说明你清楚一个事实:技术本身不新,但落地的完整度依然是大多数毕设和商业项目之间最大的分水岭。这个课题涵盖了微服务入门、数据可视化、消息队列、缓存策略、权限控制、地图API集成等一整条Web开发链路,对即将毕业的同学来说是最稳妥的选题;对想转行Java开发的人来说,它也是简历上最容易展示综合能力的一类项目。这里的"智慧"不是加几个AI接口就算数,而是要真正打通“游客出发前规划—游玩中导览—游玩后分享”这条完整业务闭环。这篇文章我会从需求拆解、技术选型、数据库设计到前后端联调、并发安全、部署上线都过一遍,重点讲清楚每一步的为什么和常见的坑,而不是只堆一个能跑的Demo。
1. 系统整体设计与需求拆解
1.1 课题目标与我为什么建议选择单体+模块化拆分
拿到这个课题后,不要着急写代码。先想清楚一件事:你做的系统到底要给谁用、解决什么问题。很多同学一上来就设计十几个表、五六种角色,最后自己维护不过来,答辩的时候也被问得漏洞百出。
我的建议是:面向游客和景区管理员两个核心角色,做一套线上线下一体化的旅游服务系统。游客端覆盖景区信息查询、门票预订、在线支付、路线推荐、订单管理、游客评论;管理端覆盖景区资源维护、门票库存管理、订单审核与退款、用户管理、数据统计看板。这个范围既能体现业务完整性,也不会因为追求大而全把自己拖垮。
至于架构选型,Spring Boot + MyBatis-Plus 的经典单体结构足够支撑这个课题展现全部技术点。你可能觉得微服务更炫,但毕设答辩的核心不是看你能拆多少个服务,而是看你能不能把某个模块的完整链路讲深讲透。单体架构下做模块化拆分——按业务边界分成 system、travel、order、user、statistics 等模块,后面如果想演进成微服务,也只需要按模块边界拆分,不会推倒重来。
1.2 角色权限与核心业务流的梳理
用户侧的角色我认为需要拆成四类:未登录游客、普通注册用户、景区运营人员、系统管理员。未登录游客只能浏览景区详情和公开的旅游攻略,一旦涉及预订、支付或者写评论就必须登录;这个小的权限边界设计,帮你天然规避了大量需要处理"游客临时数据"的业务复杂度。
核心业务流可以抽象成三条主链路:
第一条是"查景区——看详情——下单——支付——生成订单——发布评论"的完整消费链路。这条链路一定要走到支付环节,哪怕接入的是支付宝沙箱或者微信支付沙箱,也必须把支付回调、订单状态修改、库存扣减放到同一个事务闭环里。这样答辩时能直接拿"支付掉单""超时关单"这类高并发下的经典问题来展示你的思考深度。
第二条是"景区管理——门票库存调整——订单核销——数据统计"的运营链路。管理端要能实时看到每个景区的售出率、销售额、游客来源地分布,这些你后面做ECharts可视化大屏时直接复用同一套接口。
第三条是"用户行为采集——热门景区推荐——个性化路线生成"。这条链路很多人会做成摆设,我建议你哪怕只用简单的标签匹配算法,也要把推荐结果真实展现在首页,这比在旁边放一个光鲜的AI概念要扎实得多。
1.3 为什么单机部署的Spring Boot依然是这类课题最优解
有一类误区是:既然课题叫"智慧旅游",就必须上微服务、上消息队列、上分库分表。实际上对于毕设课题来说,技术选型要和业务体量匹配。一个普通景区单日游客峰值也就两三万,这在单台8G内存的服务器上,用Spring Boot配合Redis缓存完全能扛下来;硬上微服务带来的服务发现、分布式事务、链路追踪问题,反而会把核心业务淹没在环境配置里。
真正应该花精力去打磨的是:能不能写出性能合格的SQL、能不能把接口响应时间控制在200毫秒以内、能不能在订单提交时通过Redis+Lua脚本解决库存超卖问题。这些问题才是面试官会追着问的。单体架构下做垂直审视,比分布式架构下做水平堆砌,更能体现一个开发者的技术判断力。
2. 技术选型与核心原理分析
2.1 Spring Boot家族的组件搭配逻辑
Spring Boot 2.7.x 是截至2026年初依然维护最稳定的版本线,JDK 用 1.8 或 11 都行。如果是新项目,直接用 Spring Boot 2.7.18 + JDK 11 的组合,遇到问题能找到的社区答案最丰富,不容易踩版本坑。
核心组件搭配是这样的:Spring MVC处理RESTful接口,MyBatis-Plus负责持久层CRUD,Spring Security + JWT做无状态认证,Redis做缓存和分布式锁,MySQL 8.0存业务数据。这里面储物不足的是Spring Security的配置复杂度,很多人会被过滤器链绕晕。我建议你换个思路:不用Spring Security的繁琐配置,自己写一个基于JWT的拦截器 + 自定义注解(@RequireLogin、@RequireAdmin)来做权限控制。这并不是偷懒,因为你会在这个过程中彻底搞懂过滤器、拦截器、HandlerInterceptor和参数解析器的工作机制;而且讲真,毕设项目里自定义拦截器方案比Spring Security的默认配置更灵活,写进简历也能讲得更清楚。
2.2 ORM框架:MyBatis-Plus相对JPA更适合报表统计
如果你对SQL比较熟悉,就选MyBatis-Plus;如果你更习惯面向对象思维,那Spring Data JPA上限更高。但放到智慧旅游这个场景里,我明确推荐MyBatis-Plus。
原因很简单:旅游系统里有大量多表联查和统计报表需求,比如"查询每个景区本月每日的售票趋势",这种SQL用JPQL写起来很难受,但用MyBatis-Plus的LambdaQueryWrapper配合自定义XML里的复杂SQL,状态管理非常清晰。而且MyBatis-Plus内置的分页插件性能比JPA的分页更可控,处理百万级订单表时优势明显。
这里给个小技巧:不要在主业务表上直接做聚合统计,单独建一张stat_daily_report汇总表,每天晚上通过定时任务跑批把前一天的订单汇总进去。查询当日数据时直接查这张汇总表,页面加载速度能提升一个数量级。
2.3 缓存设计:Redis解决哪些问题、不解决哪些问题
Spring Boot默认支持ConcurrentMapCache,这只适合单体调试,生产环境要用Redis。Redis在智慧旅游系统里承担三件事:缓存不常变化的景区基础信息、缓存用户登录token、承担库存扣减的原子操作。
我的习惯是给缓存key设置统一前缀和过期时间,比如travel:scenic:{id},景区详情缓存30分钟,订单热点数据缓存5分钟,token缓存2小时。还有一个谁做谁爽的小设计——用Spring Cache的@Cacheable注解配合自定义KeyGenerator,可以统一生成前缀,避免团队里key格式五花八门。
特别注意:Redis不解决缓存与DB的一致性难题,只能缓解。我的方案是先更新数据库,再删除缓存。主动删除失败就靠TTL兜底,保证最终一致性。系统里评论数、浏览量这类高频更新字段,不要每次+1都命中DB,先放Redis做原子自增,定时任务每5分钟同步一次数据库。
2.4 MySQL表结构与索引设计的核心思想
数据库表设计是答辩高频区。智慧旅游系统至少需要这些表:用户表、景区分类表、景区信息表、景区图片表、门票SKU表、订单表、订单明细表、支付流水表、评论表、收藏表、用户浏览记录表、统计日汇总表。
表关系尽量控制在两层以内,避免过度抽象。底色策略是:景区信息和门票SKU分开存。同一景区可能同时存在成人票、儿童票、学生票等多种票价,把不同价格的票单独拆成SKU表,后续做库存和价格管理会灵活很多。
索引设计有一点务必注意:订单表中user_id和status要建联合索引,查询用户历史订单就是高频SQL;支付流水表里order_no如果是按雪花算法生成的,直接建唯一索引;评论表关联景区ID和时间字段,也要建联合索引。别信“索引建越多越好”这种话,索引是占空间拖写入的,一般单表索引控制在五个以内。
3. 数据库设计与核心功能落地
3.1 核心业务表结构的具体设计
表结构我直接给一个能用的方案,字段说明也标注清楚。
用户表核心字段:id、username、password(bcrypt加密存储)、nickname、avatar、phone、email、role(用字符串存,USER或ADMIN,别用int当枚举,答辩时解释起来麻烦)、status(0禁用 1正常)、create_time。
景区表(scenic_spot):id、name、summary(概述)、detail(富文本)、category_id、province、city、address、longitude、latitude、cover_image、open_time、closed_time、traffic_info(交通指南)、view_count、status。
门票SKU表(ticket_sku):id、scenic_id、ticket_name、price(用decimal(10,2))、original_price、stock、sold_count、valid_date_start、valid_date_end、refund_policy。
订单表(orders):id、order_no(20位左右雪花ID,对外展示)、user_id、scenic_id、sku_id、quantity、total_amount、status(0待支付 1已支付 2已核销 3已退款 4已取消)、pay_type、pay_time、contact_name、contact_phone、visit_date、expire_at、create_time。
评论表(comment):id、order_id、user_id、scenic_id、content、rating(1-5分)、images(JSON字符串存最多9张图)、reply_content、create_time。
浏览记录表(user_browse_history):id、user_id、scenic_id、browse_time。这表会越来越大,建议定期归档删除90天前的数据。
每张表都要有create_time,用datetime类型。id统一用bigint自增还是雪花ID?订单表用雪花算法,其余表自增就行。分页查询时大数据量下的深分页问题要注意,后面问题排查章节我会讲。
3.2 接口层的RESTful规范与统一返回体设计
接口设计看起来是小事,但直接决定前后端联调效率。统一返回体我习惯用Result<T>泛型,包含:code、message、data。成功code为200,业务失败code为400xx,未登录401,无权限403,服务器异常500。分页返回体单独做PageResult<T>,包含records、total、current、size。
接口URL风格统一用复数名词:/api/v1/scenics(GET列表)、/api/v1/scenics/{id}(GET详情)、/api/v1/orders(POST下单)、/api/v1/orders/{id}(GET详情)。所有接口统一加/api前缀,方便后面统一做网关路由或者Nginx反向代理。
这里有个典型的返回体设计坑:不要把数据库字段直接暴露给前端。比如orders表里的status字段,前端期望知道的是"待支付""已支付"这类文字状态,不是0、1、2的数字。我建议返回给前端时统一转换成VO对象,比如OrderVO里放status_desc字段,由后端把数字转换成中文。虽然麻烦一点,但省去前端自己翻译的破事,也显得你后端设计更规范。
3.3 在线预订与支付状态机的完整设计
这是整个系统的技术重点。从用户提交订单到订单完结,中间一定要有清晰的订单状态流转。
订单状态我设计了五种:待支付(0)、已支付(1)、已核销(2)、已退款(3)、已取消(4)。流转关系:待支付可以取消或者支付后变成已支付;已支付可以申请退款变已退款,或者核销后变成已核销。禁止从已支付直接跳待支付,这类非法流转在后端要做状态校验,不能只靠前端隐藏按钮。
下单链路这样做:用户提交下单请求后,后端先生成订单记录(状态为待支付),扣减Redis中的库存(用Lua脚本保证原子性),接着调用支付宝沙箱或微信支付沙箱生成预支付单,把支付二维码返回前端。用户扫码支付成功后,支付平台异步回调后端通知接口。后端收到回调后,校验签名和金额,再更新订单状态为已支付,同时累加SKU的已售数量。这里最关键的保证是:支付回调处理接口必须对请求参数做验签,且必须做到幂等——同一条支付通知可能因为网络原因多次送达,后端要做状态判断,只有当前状态是待支付的订单才更新为已支付。
超时关单的逻辑可以这样实现:订单创建时设置过期时间(比如10分钟),用定时任务每分钟扫描一次所有待支付且过期时间小于当前时间的订单,批量取消。不用RabbitMQ延迟队列,是为了减少项目部署时的额外依赖。答辩时如果被问到"为什么不用延迟消息",你可以答:延迟队列确实更优雅,但考虑到部署成本和实际请求量,定时扫描已经满足需求,这也是一种合理的技术取舍。
3.4 管理端数据统计与可视化大屏的实现要点
管理端要有一个带数据可视化大屏的首页。我用ECharts来做柱状图、折线图、饼图,数据源就用前面提到的stat_daily_report汇总表。
大屏左侧展示景区门票销售额Top10,用横向柱状图;中间展示近30天每日订单量和销售额趋势,用双Y轴折线图;右侧展示游客来源地分布,用饼图;底部是实时订单滚动列表,用WebSocket模拟推送。
ECharts图表的数据接口要单独写,不要和业务接口混在一起。后端返回的格式要迎合ECharts的要求,比如饼图需要[{name: '北京', value: 1024}, {name: '上海', value: 2048}]这样的结构,你在后端组装好再返回,前端拿数据直接塞进option就能渲染。
可视化大屏的坑非常隐蔽:页面首次加载时全部图表同时请求接口,会把后端打懵。建议做一个队列,等页面所有DOM节点就绪后再逐个加载图表,或者给每个图表接口加一个20~50ms的随机延迟,错峰请求。
4. 项目实操过程与核心环节实现
4.1 IDE选型与项目初始化配置
工具建议:IntelliJ IDEA(社区版够用,旗舰版更好)、Navicat(数据库管理)、Postman或Apifox(接口调试)、Git(版本管理)。我这里默认你会用Maven做依赖管理,不会的话先花半小时学一下Maven的依赖坐标、仓库和生命周期。
初始化Spring Boot项目时,Spring Initializr上勾选这些依赖:Spring Web、MySQL Driver、Lombok、Validation。MyBatis-Plus、JWT、Redis这些依赖手动加进pom.xml,版本号用Maven BOM统一管理。建议创建项目时就建好application-dev.yml和application-prod.yml两个环境配置文件。
application-dev.yml里,数据源、Redis、日志级别、JWT密钥这些全部配置好。重点提一下日志配置:生产环境别用默认的Spring Boot日志格式,配上logback-spring.xml,按天滚动,保留30天,日志级别INFO。错误日志单独输出到一个error.log文件。这样系统运行出问题,排查效率高得多。
4.2 我的本地开发环境描述
一套省心的本地环境是高效开发的起点。我自己现在的配置是:Windows 11 + JDK 11 + Maven 3.8.x + MySQL 8.0 + Redis 6.2。如果你的电脑配置有限,关掉部分IDE插件也能跑得很流畅。
MySQL装好后记得建库设字符集为utf8mb4,排序规则utf8mb4_general_ci即可。数据库连接串上加useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai,时区必须设置,否则日期查询会有8小时偏移。
初始化数据不要只用假数据。部分基础数据可以从无版权景区信息源导入,但要注意把导入的图片转存到本地静态资源目录,避免外链图片挂掉。每个景区配5~8张图片,覆盖不同角度和季节,这样前端页面展示效果完全不一样。
4.3 核心代码实现:在线预订的后端链路
我挑最核心的在线预订功能,把后端链路完整走一遍。
用户提交下单请求时,Controller接收CreateOrderRequest,参数包括:scenicId、skuId、quantity、visitDate、contactName、contactPhone。代码分三层:
Service层方法createOrder()里做这些事:
- 校验SKU是否存在且状态为可售;
- 校验游玩日期在SKU的有效期内;
- Redis Lua脚本扣减库存,这里务必用Lua脚本保证“检查库存充足再扣减”和“扣减”两个动作是原子的;
- 插入订单表,状态待支付;
- 调用支付服务生成支付单;
- 返回下单结果和支付二维码链接。
public OrderVO createOrder(CreateOrderRequest request, Long userId) { TicketSku sku = skuService.getById(request.getSkuId()); if (sku == null || !sku.getStatus()) { throw new BizException("该门票已下架"); } if (request.getVisitDate().isBefore(LocalDate.now())) { throw new BizException("游玩日期不能早于今天"); } // Redis Lua脚本原子扣减库存 boolean success = stockService.deductStock(request.getSkuId(), request.getQuantity()); if (!success) { throw new BizException("库存不足"); } try { Orders order = buildOrder(request, userId, sku); order.setStatus(OrderStatus.WAIT_PAY); ordersMapper.insert(order); // 调用支付网关,生成支付单 String payUrl = payService.createPrepayOrder(order.getOrderNo(), order.getTotalAmount()); return OrderVO.from(order, payUrl); } catch (Exception e) { // 订单创建失败,回补库存 stockService.restoreStock(request.getSkuId(), request.getQuantity()); throw e; } }这代码有两个细节必须掌握:第一,Redis扣库存成功但数据库插入失败,是一定要回补Redis里的库存,否则会出现用户看到库存充足但下单失败的隐性Bug;第二,buildOrder生成订单号时用雪花算法,确保全局唯一。
4.4 用户端体验优化:多维度发现与游玩助手
光能下单不够,智慧旅游系统的用户端要让游客"找得到、看着爽、来得值"。我建议实现几个区别于普通CRUD的功能点。
第一个是"多维度景区搜索":用户可以按省份、城市、景区分类、门票价格区间、评分区间组合筛选。这功能听起来简单但做起来有细节,比如城市筛选时,前端传的是拼音缩写还是城市ID?建议统一用城市ID传递,拉数据时JOIN城市表。价格筛选用between条件,范围内的SKU最低价作为景区的展示价格,用MySQL的MIN()聚合,配合HAVING过滤。这些SQL都是面试官爱问的高频SQL。
第二个是"游玩助手"功能:景区详情页除了基础信息,增加一个由Lombok构建的景点游玩时长估算工具条。根据游客选择的不同景点组合,动态估算总游玩时间,给出推荐行程安排。这个功能可以用简单的贪心算法实现:把用户选择的景点按地理位置排序,相邻景点之间加上交通时间,累加每个景点的建议游玩时长。做出来之后,演示效果极强——答辩现场直接演示"一键生成我的专属游玩路线",比讲十页PPT都管用。
第三个是"内容互动":游客游玩后可以发带图评论,管理员可以置顶优质评论。评论列表要支持按"热度"和"时间"两种排序方式,热度按评论点赞数计算。点赞功能不要单独建一张点赞表然后关联查询,直接Redis里的Set结构存储每个评论的点赞用户ID集合,页面展示时只查SCARD获取数量就行,能省大量查询。
4.5 管理端实操流程:从上架到退款全流程
管理端的核心操作路径:管理员登录后,先进入"景区管理"创建景区基础信息,上传图片和交通指南;再进入"门票SKU"给景区配置不同票种;用户端开始产生订单后,管理员在"订单管理"看到订单列表,可以对某笔已支付订单执行退款操作。
退款这步要特别注意:退款不是简单改订单状态,要调用支付平台退款接口,退款成功后系统收到异步通知,才能把订单状态改为已退款。整个流程要有状态机保护,防止重复退款。我在项目里给退款操作加了一个refund_status字段:未退款(0)、退款中(1)、退款成功(2)、退款失败(3)。当后台发起退款时,立刻把状态改成退款中,同时加一把Redis分布式锁,防止并发点击多次退款。退款成功回调后更新为退款成功,再更新订单主状态。
这套状态机保护是我强烈建议你加的,体现的是工程意识,不是学生思维。
4.6 前后端分离部署与跨域问题处理方案
智慧旅游系统建议做成前后端分离:Vue 3 + Element Plus 做后台管理,uniapp 或 H5 做用户端(如果做App选uniapp,能一套代码多端打包)。前后端分离下,跨域问题必须处理。
最稳妥的方案是:后端单独写一个CorsConfig配置类,允许指定前端域名跨域,不需要开发时关闭浏览器安全策略。配置时allowedOriginPatterns用"*"配合allowCredentials(true),注意不能用allowedOrigins("*"),后者和携带Cookie不兼容。
生产环境部署时,Nginx统一监听着80端口,把/api路径反向代理到后端的8080端口,把/路径指向前端静态文件目录。这样不存在跨域问题。只有本地开发时需要开启CORS配置。我建议本地开发也使用Nginx做代理转发,从开发环境就模拟生产环境,减少"本地通、线上挂"的概率。
5. 常见问题与排查技巧实录
5.1 分页查询卡死与深分页SQL优化
我遇到过最恼火的性能问题之一,是后台管理分页翻到第200页时接口响应要5秒以上。这是因为LIMIT 19800, 20是让MySQL扫描前面19800条记录,再丢弃掉,只返回最后的20条。数据量过万后这种查询极慢。
解决方案是用子查询延迟关联:先由覆盖索引拿到本次分页需要的20个主键ID,再用这些主键ID去主表取整行数据。改写后的SQL大约是:
SELECT * FROM orders WHERE id IN ( SELECT id FROM orders WHERE user_id = 10086 ORDER BY id DESC LIMIT 19800, 20 )这个方法在百万级数据下能把响应时间从5秒压到200毫秒以内。答辩时把这条SQL优化案例主动讲出来,面试官通常都是点头的。
5.2 高并发下的库存超卖问题与解决方案对比
超卖是毕设答辩必问的压轴题。如果你真的遇到了"库存剩1件却被两个人同时买走"的场景,原因一定是扣减库存时没有加原子性保护。
解决方案我至少要会三种:数据库乐观锁(UPDATE stock = stock - 1 WHERE stock > 0)、悲观锁(SELECT FOR UPDATE)、Redis分布式锁+Lua脚本。三种方案我都实测过,真实感受是:
- 乐观锁在低冲突场景性能最好,但高并发下重试次数多,体验一般;
- 悲观锁实现简单,但锁等待会让吞吐量掉得厉害;
- Redis Lua脚本是这套系统的最优解,扣减逻辑和查询逻辑在一个脚本里原子执行,吞吐量高且不会超卖。
Lua脚本长这样:
local stock = redis.call('GET', KEYS[1]) if tonumber(stock) >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 end return 0发布一个补充知识点:支付的幂等处理讲得通,基本上支付链路就能过关。我们的支付回调接口收到支付平台的通知后,先查订单当前状态,如果已经是"已支付",直接返回成功并丢弃本次通知,这样就不会重复加钱、重复发货。
5.3 API接口安全防护与数据校验经验
安全是这类系统最容易忽略的地方。最少要做到三件事:
第一,参数校验用@Validated注解做强制校验,DTO对象里@NotNull、@NotBlank、@Min、@Max全部配上。千万别只在写SQL前手动判断,代码又臭又容易漏。
第二,防止SQL注入。MyBatis-Plus的QueryWrapper底层是预编译,所以参数不用太担心,但写XML自定义SQL时,如果用了${}拼接参数,先对参数做白名单校验,能用#{}绝不用${}。
第三,用户只能访问自己的资源。比如查看订单详情接口,userId不从前端传,而是从JWT上下文中获取,再校验这个订单确实属于这个用户。防止水平越权漏洞,防止用户直接改URL里的订单号去查别人的订单。
5.4 环境问题排查:时区八小时、Redis连不上、跨域失效
这里把我实际遇到的问题列一个速查表:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 数据库查询结果比当前时间慢8小时 | JDBC连接串没设置时区 | serverTimezone=Asia/Shanghai |
| Redis报连接超时 | 默认只监听127.0.0.1 | 修改bind 0.0.0.0,设置protected-mode no或配置好密码 |
| 前端跨域报错 | Nginx未配置或CORS类没加 | 按5.6节方式排查,最关键是allowedOriginPatterns别写成allowedOrigins |
| 部署后上传图片无法访问 | 图片存到了后端/tmp目录 | 修改spring.servlet.multipart.location为固定目录,用Nginx映射本地磁盘路径 |
| 本地能跑、线上打jar包后报找不到静态资源 | 文件和jar包分离不规范 | 静态资源和模板路径不使用classpath:/,配置外部文件路径映射 |
跨域这个问题很奇怪,有时开发环境配好了,部署到服务器后又失效。原因一般是:Nginx层做了跨域拦截,优先级高于后端CORS配置;或者浏览器对预检请求OPTIONS没放行。建议在Nginx配置里统一加上跨域头,然后后端不再单独配置CORS。回源请求由Nginx拦截后全部加上Access-Control-Allow-Origin头。
5.5 部署落地与答辩演示时的注意点
部署阶段的坑,我觉得讲三天三夜也讲不完。最简单的稳妥做法:阿里云轻量应用服务器(2核4G,带宽5Mbps够用) + Docker部署 + Nginx反代。用docker-compose把MySQL、Redis、后端、前端四个服务编排起来。
部署时留三个心眼:
- MySQL的数据目录一定要挂载到宿主机,容器重造后数据不丢失;
- 用Envoy或conf.d文件把3306、6379端口绑定到127.0.0.1,不要直接暴露公网;
- 后端容器里用环境变量注入数据库密码,别把密码硬编码到yml文件里,答辩时万一被问到Docker安全也有话说。
答辩演示时最怕当场翻车。我的建议是准备一套完整的演示脚本,演示每个功能前先复述"我要演示X,预期看到Y",即使网络出问题,也能展示你对业务的掌控力。手机开热点做备用网络;本地开发环境再跑一份,万一线上挂了立刻切本地。
6. 并发与性能测试的基本功
6.1 哪些接口一定要做性能测试
接口性能要提前心里有数。我的经验是:系统里这几个接口必须做压力测试——景区列表页查询接口、景区详情接口、下单接口、支付回调接口。
压测工具首选JMeter,写个简单线程组就能用。没有JMeter的,Postman的Runner也能按固定次数跑并发。更偷懒的用法是直接用Linux下ab命令:
ab -n 10000 -c 500 -T application/json -p order.json http://localhost:8080/api/v1/orders这个命令意思是:模拟500并发,总共打1万次下单请求,观察TP99响应时间和错误率。
以我经验,普通服务器上单接口QPS能到500以上,TP99小于300毫秒,就算可以了。不达标就逐个排查:先看有没有慢SQL(开启MySQL慢查日志),再看Redis缓存命中率,再看GC日志。压测能暴露的隐患,远比写代码时能预想到的多。
6.2 实测中的性能瓶颈定位思路
我参与过的一个模拟项目压测时发现,下单接口在200并发时就开始大量超时。用Arthas跟踪到热点是stockService.deductStock()里没走Lua脚本,而是先查Redis再在Java层判断再扣减,这期间有并发窗口导致超卖,同时Redis连接数也打满了。
优化方案两步走:第一,扣减逻辑换成Lua脚本单命令原子执行;第二,Redis连接池最大连接数从8调成50,从Jedis换成Lettuce,Lettuce底层是Netty,支持多路复用,天然比Jedis省连接。改完后压测,500并发下单接口TP99稳定在200毫秒内,库存也没发生过超卖。
这条经验特别适合写进项目的中文技术博客:“一次从性能瓶颈到接口优化的实战复盘”。
6.3 压测时数据库连接池参数调优
Spring Boot默认的HikariCP连接池性能已经很优秀,但默认配置并不适合所有场景。压测时数据库连接是最容易先耗尽的那一层。
我常用的HikariCP参数:
maximum-pool-size: 常规项目配20~50,压测时可以临时调高到100;minimum-idle: 等于maximum-pool-size,避免空闲时频繁创建连接;connection-timeout: 30000,超过30秒拿不到连接直接报错,避免长期阻塞。
这些参数写死前,最好通过压测来确认。你会发现一个规律:数据库连接数不是越大越好,连接数增大往往意味着数据库线程切换开销变大,性能反而下降。压测时看一眼数据库监控指标,找到连接数拐点,再把参数定下来。
7. 从毕设到简历的项目包装建议
7.1 项目README与数据库设计文档的整理技巧
如果你打算把这个项目放进简历,文档整理这步千万别省。
项目根目录放一个README.md,内容至少包括:项目背景与目标、系统架构图(用文本或者PlantUML画,答辩PPT也能用)、技术栈清单、快速启动指南、核心业务流程说明、接口文档链接。接口文档用Apifox或Knife4j自动生成,省事又专业。
数据库设计文档建议整理成一份database-design.md,把每张表的字段、索引逻辑、表关联关系写清楚。面试官问"你为什么在这个字段上建索引"时,你能直接说出判断依据,这比写一堆CRUD代码值钱多了。
7.2 答辩时如何讲清楚系统的技术亮点
答辩现场真正的高光时刻,是你能不能把某个模块讲深一个层级。比如讲到Redis库存时,不要只说"用了Redis做缓存",而是说:“门票库存这块的并发场景,早期用数据库乐观锁解决超卖,后来压测发现200并发时性能下降明显,最终用Redis Lua脚本替代,500并发下QPS突破一千,库存准确性也能保证。”
这种讲法背后是踩坑的深度,装不出来。所以写代码阶段就要有意识地记录这类“从坏到坏、从坏到好”的优化路径。
7.3 项目后续升级方向的思考
如果学有余力,还可以加入这些扩展方向:
- 接入基于位置的服务(LBS),实现“附近热门景点”推荐;
- 引入ElasticSearch,用搜索中间件替换MySQL的LIKE模糊查询,提升搜索体验;
- 用消息队列削峰填谷,订单创建和生产入库解耦;
- 引入对象存储服务,图片上传不再落在本地磁盘。
不过我的建议是先把核心链路的完整度打磨好,再考虑加法。一个能稳定跑通支付、退款、统计、权限的项目,已经比大多数只做了CRUD演示的同龄人强很多了。
8. 写在最后的实务经验
如果你打算在2026年把"基于Spring Boot的智慧旅游系统"作为毕业设计或者求职项目,我的体会是:这个课题真正的分水岭不在技术本身,而在于你有没有把业务闭环打磨完整。很多同期的人做到"能跑"就打止了,而你要做到"好用、讲得清、扛得住追问"。多花时间在支付回调和库存一致性上,获得的理解深度比多做两个花哨页面有价值得多。
最后送一个实用小技巧:把前后端代码都传到一个私有仓库里,每次上线前打一个tag。遇到线上问题排查时,你看着tag版本配合Nginx访问日志和健康检查接口,十分钟就能定位是代码问题还是环境问题。这套工作流看起来很简单,但很多人就是没养成习惯,一次线上事故就能折磨一周。
希望这篇文章对你做这个课题有用。所有推荐方案我都实际验证过,你照着做至少不会出现大方向上的偏差;至于更细节的坑,欢迎在评论区留下你的具体报错,我看到都会回复。