
做航空票务类项目的时候很多人第一反应就是“订票、查航班、后台管理”这三板斧做完能跑就算交差。但如果你接触过真实项目或者毕业设计评审会发现真正拉开差距的恰恰是“推荐”这两个字。我这次做的这个基于 Java SSM Flask 的航空票务推荐系统核心并不是把机票 CRUD 做得多花哨而是把用户行为数据利用起来给不同的人推不同的航班组合。这篇文章我把整个项目的设计思路、推荐算法的落地方式、SSM 和 Flask 两个服务怎么协作以及我在联调和部署时踩过的坑都整理出来希望能给正在做同类选题的同学或刚入行的后端开发者一些直接能用的参考。先说下系统定位。它不是一个简单的机票查询网站而是一个带有个性化推荐能力的票务服务平台。用户登录后系统会根据他浏览过的航线、收藏过的航班、历史订单的舱位等级在首页和搜索结果页生成“猜你喜欢”“相近出发地推荐”等模块。管理端则支持航班管理、订单统计、用户画像查看。技术栈选了 Java SSM 作为主后端Flask 作为推荐算法的独立服务两者通过 HTTP 接口沟通。这套结构的好处是业务代码和算法代码解耦哪怕你不会在 Java 里写复杂的协同过滤也能用 Python 快速实现推荐逻辑而且后期替换算法模型也不影响主系统。1. 项目整体设计与需求拆解1.1 这个系统到底要解决什么问题先别急着写代码把需求想清楚。航空票务系统表面上要管航班、乘客、订单实际上面临三个核心痛点。第一个痛点是信息过载。国内每天在飞的航班数量非常多再加上中转组合用户搜索一个“北京到上海”可能返回几十条结果。普通用户没有耐心一页页翻他需要系统帮他过滤掉不合适的航班。第二个痛点是用户偏好难以捕捉。有人只坐早上八点前的航班有人只选宽体机有人是价格敏感型。这些偏好如果你不做记录每次都得用户重新输入筛选条件。第三个痛点是收益管理。对平台方来说把高佣金航班或者合作航司的航班推给合适的人能提高转化率和收益这也是推荐系统在商业上的价值。我做的这个系统不是纯理论 demo而是把这三件事落到了具体功能上。用户的行为数据会被埋点收集比如点击查询、查看详情、加入收藏、最终下单。这些数据进入推荐服务后会生成用户兴趣向量。首页的推荐位会根据兴趣向量动态变化而不是所有用户看到一样的“热门航线”。另外这个系统还需要满足常见的交付要求源码、LW论文/设计文档、调试文档、讲解视频或 PPT。所以我在架构上刻意保持了模块边界清楚写文档的时候能画得出架构图讲代码的时候能分清楚哪段是业务逻辑、哪段是算法逻辑。这一点后面我会专门说。1.2 为什么选择 JavaSSMFlask 这套组合选型这件事我见过太多人纠结。其实不用迷信技术栈要看项目目标和维护成本。SSM 是指 Spring SpringMVC MyBatis这是 Java Web 开发里非常经典的一套组合。Spring 负责对象管理和事务SpringMVC 负责请求路由MyBatis 负责数据库操作。这套组合在课程设计、毕业设计和中小型公司项目中都非常常见相关参考资料特别多遇到问题容易搜到解决方案。更重要的是大多数评审老师或者面试官都熟悉这套技术你讲项目的时候不需要花时间解释框架原理。那为什么还要加 Flask因为推荐算法用 Python 写更顺手。Java 也能写协同过滤但 Python 那边有 pandas、numpy、scikit-learn 这些库处理用户行为矩阵、计算相似度非常方便。Flask 是 Python 里最轻量的 Web 框架我用它包装一个推荐接口接收 Java 传来的用户 ID 和场景类型返回推荐结果列表。Java 端只需要用 HTTP Client 调用 Flask 接口即可。有人会问直接 all in Java 不行吗行但会增加工作量。比如你要在 Java 里做矩阵运算得引 Apache Commons Math 或者其他库代码量变大调试也不直观。而用 Flask 单独做推荐服务我可以先在本地用 Jupyter 把算法跑通再写成一个接口效率高很多。这也是为什么现在很多大厂会把推荐、风控这类算法模块拆成独立服务的原因——技术栈隔离团队可以各干各的。2. 核心功能模块与推荐算法细节2.1 用户、航班、订单核心模型设计数据库表设计是这类系统的地基。我总共设计了 6 张核心表用户表、航班表、航线表、订单表、收藏表、用户行为日志表。这里重点讲几个容易忽略的设计点。航班表不能只有简单的航班号、起降时间还得有飞机类型、舱位代码、餐食信息、准点率等字段。为什么因为推荐算法需要这些特征来计算相似度。比如用户历史订过“波音 787 的宽体机”下次推荐时就要优先推送宽体机航班。如果表里没有机型字段算法就无从下手。订单表要关联用户 ID、航班 ID、舱位等级、票价、下单时间。这里我用了逻辑删除而不是物理删除因为用户取消订单后历史记录仍然可以作为推荐依据。你不能因为用户退了一次票就把他以前的偏好全部抹掉。用户行为日志表是我自己加的一张表。它记录的行为类型有五种search、click、collect、book、cancel。每条日志包含用户 ID、航班 ID、行为类型、时间戳。这张表是推荐系统的“原材料”。一开始我没打算建这张表觉得有订单表和收藏表就够了后来发现不行——因为用户可能只是点击查看或者搜索了某个航线但没有下单这些隐含偏好恰恰是很重要的信号。2.2 推荐逻辑从冷启动到协同过滤推荐算法我分了两条线走冷启动处理和基于物品的协同过滤。冷启动是指新用户没有任何行为数据的情况。这时候推荐什么我的做法是基于流行度推荐 基于规则的推荐。流行度就是统计所有用户最近七天的点击量、收藏量、下单量加权算出一个热门分数排序后取 Top N。规则推荐是指根据用户当前搜索的城市对推荐同时段的替代航班。比如用户搜了“北京-上海 上午”如果上午的直飞售罄系统可以推荐邻近时间或者经停航班。这个规则不依赖用户历史是纯业务逻辑。当用户行为数据积累到一定量比如收藏了 3 个航班、下过 2 个订单系统就切换到协同过滤。我用的方案是 Item-Based CF也就是根据用户对航班的评分计算航班之间的相似度。先看公式sim(i, j) (用户同时喜欢航班 i 和航班 j 的数量) / sqrt(喜欢 i 的用户数 * 喜欢 j 的用户数)。这就是余弦相似度的一种变体计算简单效果稳定。实际操作时我不会每次请求都重新算全量相似度那样太慢。我的做法是每天凌晨跑一个离线脚本把当天的用户行为数据导出来计算航班相似度矩阵存成 JSON 文件或者写回数据库的推荐表。Java 端在用户访问首页时只需查询这张推荐表不需要实时调用 Flask 计算。推荐质量的刷新周期是 24 小时对机票这种低频高价商品来说足够了。2.3 Flask 在其中扮演的角色算法服务分离Flask 服务我起了两个端口级接口一个是 /api/recommend支持传入用户 ID返回个性化推荐航班列表另一个是 /api/hot返回热门航班。这两个接口都返回 JSONJava 端用 RestTemplate 调用。做算法服务分离有一个很现实的好处调参不用重新编译 Java 工程。比如我想调整协同过滤中余弦相似度的阈值或者修改冷启动的加权系数只需要在 Python 代码里改重启 Flask 进程就行。而 Java 后端完全不需要动这在大项目里能省不少事。Flask 的接口我设计成响应结构统一{ code: 0, data: { items: [ { flightId: 101, score: 0.87 } ] }, message: success }。Java 端拿到后再根据 flightId 去数据库查完整航班信息组装成页面需要的 DTO。这样推荐服务不直接依赖数据库的具体表结构两边只通过 ID 关联耦合度很低。3. 实操过程搭建环境与关键代码实现3.1 环境准备与项目骨架我用的是 IntelliJ IDEA Maven JDK 8 MySQL 5.7 PyCharm Python 3.8。为什么选 JDK 8因为 SSM 框架生态里JDK 8 是最成熟稳定的版本网上排错资料多遇到内存参数问题也好解决。Maven 用来管理依赖Spring、MyBatis 的包版本我统一用了 5.x 和 3.5.x 这几个比较稳的版本。项目骨架分成两部分airline-recommend/ ├── backend (Java SSM工程) │ ├── src/main/java/com/airline/ │ │ ├── controller/ # Controller层 │ │ ├── service/ # 业务逻辑层 │ │ ├── dao/ # MyBatis Mapper接口 │ │ ├── model/ # 实体类 │ │ └── config/ # Spring配置 │ ├── src/main/resources/ │ │ ├── mapper/ # MyBatis XML文件 │ │ └── spring/ # Spring配置XML │ └── pom.xml ├── recommend-service (Flask服务) │ ├── app.py │ ├── recommend.py # 推荐算法 │ ├── data_process.py # 行为数据导入 │ └── requirements.txt ├── docs/ # 设计文档、数据库脚本 └── sql/ └── airline_db.sql数据库脚本建议写清楚建表语句和初始测试数据因为答辩的时候老师经常会让你现场演示没有测试数据的话推荐模块看起来就是空的。3.2 SSM 端关键配置和接口示例SSM 配置最繁琐的是 Spring 和 MyBatis 的整合。我直接把核心配置写出来这个配置能省掉很多新手期的坑。!-- spring-mybatis.xml -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/airline_db?useSSLfalseamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value你的密码/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.airline.dao/ /bean这里注意两点。第一JDBC URL 里必须加 characterEncodingutf8不然插入中文用户名的时候会乱码。第二我用了 Druid 连接池不是因为他比 HikariCP 强多少而是 Druid 自带监控页面调试的时候能直接看 SQL 执行情况对这个体量的项目很实用。Controller 层我设计了一个“统一返回体”ResultData所有接口都返回 JSONRestController RequestMapping(/api/flight) public class FlightController { Autowired private FlightService flightService; GetMapping(/recommend) public ResultData recommend(RequestParam(userId) Integer userId) { ListFlightVO list flightService.recommendFlights(userId); return ResultData.success(list); } }FlightService 里的实现逻辑是先调用 Flask 推荐服务拿到 flightId 列表再根据 ID 批量查数据库最后组装成 FlightVO。这里我踩过一个坑用 Feign 或者 RestTemplate 调用 Flask 时如果 Flask 返回的字段和 Java 实体对不上会直接报 JSON 解析异常。所以我在 Flask 端统一输出字符串类型的 ID避免 Long 类型精度丢失问题。虽然 Python 的 int 可以表示很大的数但 Java 的 Long 和 JavaScript 的 Number 之间有精度隐患这里直接用字符串最稳妥。3.3 Flask 推荐服务接口示例Flask 端我只写了两个文件一个 app.py一个 recommend.py。app.py 负责接口路由recommend.py 负责算法。# app.py from flask import Flask, request, jsonify from recommend import get_user_recommend, get_hot_flights app Flask(__name__) app.route(/api/recommend, methods[GET]) def recommend(): user_id request.args.get(userId) items get_user_recommend(user_id) return jsonify({code: 0, data: {items: items}, message: success}) app.route(/api/hot, methods[GET]) def hot(): items get_hot_flights() return jsonify({code: 0, data: {items: items}, message: success}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)推荐算法的核心我用 pandas 来实现。从数据库读出用户行为日志后构造一个“用户-航班”的评分矩阵。评分的规则是点击 1 分收藏 3 分下单 5 分取消 -2 分。这个权重是我自己调的不一定最优但能反映基本偏好。然后计算航班之间的余弦相似度取每行 TopN。# recommend.py import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_matrix(logs_df): # logs_df: user_id, flight_id, behavior, ts score_map {click: 1, collect: 3, book: 5, cancel: -2} logs_df[score] logs_df[behavior].map(score_map) # 用透视表构造用户-航班矩阵 matrix logs_df.pivot_table(indexuser_id, columnsflight_id, valuesscore, aggfuncsum) return matrix.fillna(0) def get_user_recommend(user_id): # ... 读取数据调用 build_matrix计算相似度返回推荐列表其实如果不想依赖 scikit-learn手写余弦相似度也不复杂两个航班在所有用户上的评分向量点积除以模长乘积。但既然有现成的库直接用 sklearn 更稳效率也更高。这里要特别提醒Flask 服务在生产环境不要用自带的开发服务器跑性能太差。我用的是 gunicorn 启动起了 2 个 worker。启动命令是gunicorn -w 2 -b 0.0.0.0:5000 app:app如果是 Windows 本地调试直接用 python app.py 就够了但部署到 Linux 服务器上一定要换 gunicorn否则并发一高就卡死。4. 联调、部署与踩坑记录4.1 前后端联调与跨域问题这个项目我前端用的是 JSP Bootstrap没有前后端完全分离所以联调主要发生在 Java 后端和 Flask 推荐服务之间。但如果你打算把前端换成 Vue 或者 React那就一定会遇到跨域问题。跨域的解决方案很简单在 Java 配置类里加一个 CorsFilter。我给 SpringMVC 加了一个 WebMvcConfigurerConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里注意allowedOrigins 不能写 *因为 allowCredentials(true) 的时候浏览器会拦截。必须写具体的域名或端口。我当时因为这个问题排查了两个小时一直以为是前端的问题结果就是后端配置不够严谨。4.2 常见问题排查表这条是避坑核心我把这个项目里前后端遇到的高频问题整理成了表格已经跑过几轮测试基本涵盖大部分新手会踩的雷。现象原因解决方案页面中文乱码数据库字符集不是 utf8或 JDBC URL 缺少 characterEncoding建表时指定 DEFAULT CHARSETutf8mb4URL 加 characterEncodingutf8Flask 返回 500数据库连接失败或 pandas 读不到数据确认 MySQL 开启远程访问和密码正确用 Navicat 先测连通Java 调用 Flask 超时RestTemplate 默认不设置超时时间Flask 端算法执行慢给 RestTemplate 设置 connectTimeout 和 readTimeout比如 3 秒和 10 秒推荐结果为空用户没有任何行为日志冷启动逻辑未处理调用 /api/hot 接口返回热门兜底点击行为大量丢失前端埋点代码用了同步请求阻塞页面跳转改为 navigator.sendBeacon 或异步图片请求订单取消后推荐仍出现该航班取消日志权重设置过低未能抵消历史收藏增加 cancel 权重到 -5或直接过滤最近取消的航班同一请求重复写入行为日志页面刷新时重复提交后端按照用户 ID 航班 ID 行为类型 时间窗口做去重其中“推荐结果为空”的坑最隐蔽。我第一次跑通推荐接口时用了一个新注册的测试账号结果详情页推荐位是空的看起来像系统出了 bug。后来才意识到是冷启动没做兜底赶紧在 Java 端加了个判断如果 Flask 返回空列表就默认查热门航班。这其实是所有推荐系统的通用底线逻辑。4.3 部署到服务器后的性能优化我把项目部署在一台 2 核 4G 的云服务器上Java 后端用 TomcatFlask 用 gunicorn数据库是 MySQL。刚部署完那会压测了一下并发 50 就开始出现接口超时。后来做了三件事才把性能拉起来。第一件事是给 MySQL 加索引。航班表的起降城市、订单表的用户 ID、行为日志表的用户 ID 和时间戳这些高频查询字段都建了复合索引。特别是行为日志表我建了 idx_user_time(user_id, ts)这样离线统计时查询速度提升了好几倍。第二件事是把 Flask 的批量查询改成缓存。因为热门航班和推荐结果其实可以缓存 30 分钟没必要每次请求都重新计算。我用 Redis 存了一份 JSON缓存命中的话直接返回没有命中才去调算法。改动很小但 Tomcat 和 Flask 之间的 HTTP 调用频率立刻降了下来。第三件事是开启 Tomcat 的压缩传输。在 server.xml 里配置 compressionon把 JSON 响应的体积压小尤其推荐接口返回航班列表时字段比较多压缩后能省不少带宽。5. 文档、调试与交付经验5.1 设计文档LW怎么写才能过审毕业设计或者课程设计的文档常见的问题是堆砌功能描述老师问“你是怎么实现的”就答不上来。我的经验是文档里至少要有三张图。第一张是系统架构图画出浏览器、Java 后端、Flask 推荐服务、MySQL、Redis 之间的关系。第二张是程序流程图重点画推荐算法的执行流程从行为数据采集到离线计算到在线推荐。第三张是数据库 ER 图把六张表的关系画清楚。文档的目录我建议这样安排第一章引言背景、意义、国内外现状第二章关键技术SSM、Flask、协同过滤算法第三章需求分析功能需求、用例图第四章系统设计架构设计、数据库设计、接口设计第五章系统实现每个模块的核心代码和运行截图第六章测试测试用例、测试结果。这个结构能覆盖绝大多数评审老师的关注点。调试文档也很重要。我单独写了一个 DEBUG.md记录每个模块的启动步骤、依赖配置、常见报错以及解决办法。比如 MySQL 版本是 8.0 以上时驱动类要从 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver并加时区参数。这些例子在调试文档里多写几个接手项目的人会感激你。5.2 讲解演示时如何突出重点如果你需要发送讲解视频或者做现场演示不要一上来就念需求。我的习惯是先花 30 秒说明这个系统的核心亮点——双技术栈 推荐算法。然后演示用户登录后首页推荐位的变化再切到一个新注册账号展示冷启动兜底效果。这样老师或者面试官一下就知道你做的不是搬运代码的 demo而是有真实逻辑在里面的项目。演示过程中重点说清楚一个问题为什么推荐结果会变化你可以现场操作给用户 A 收藏两个去成都的航班刷新推荐页让用户 B没有任何操作看热门推荐对比两个界面。这个对比非常直观能让人立刻理解推荐系统的价值。我还准备了一段接口测试的截图展示 Java 调用 Flask 返回的 JSON 结构和耗时。时间数据很有说服力如果接口平均响应在 100 毫秒以内说明设计和缓存方案是有效的。5.3 这段代码哪里还能继续扩展项目做完了但我会在文档最后留一个“未来改进方向”这部分其实是展示你思考深度的机会。我写的是第一把基于物品的协同过滤升级为混合推荐算法加入基于内容的特征比如机型偏好、准点率权重第二将离线计算从每天一次改成实时计算引入 Flink 流处理第三增加简单的 A/B 测试模块可以对比新旧推荐算法的点击率。写这些不需要真的实现但能说明你理解推荐系统的演进方向比空谈“系统功能完善”强得多。我在实际调试过程中最深的体会是推荐系统最难的往往不是算法本身而是数据从哪来、怎么清洗、怎么评估效果。这个项目用 Flask 单独做推荐服务让我能很快验证不同方案的差异。如果你也想参考这个结构建议把“记录用户行为”这件事想清楚——它决定了推荐模块有多大的发挥空间。另外给别人调试项目时记得把环境变量、数据库账号密码这些敏感信息单独放配置文件里别直接写死在代码中。这一点在交作业或者开源时尤其重要。