ARTICLE DETAIL

资讯详情

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

Java+SSM+Flask航空票务推荐系统:双后端架构与协同过滤实战

Java+SSM+Flask航空票务推荐系统:双后端架构与协同过滤实战 如果你在毕设选题阶段看到“基于JavaSSMFlask航空票务推荐系统”这个题目第一反应大概率是懵的Java后端已经有 SSM 了为什么还要搭一个 Flask推荐系统到底推荐什么两个后端怎么通信这个项目我完整跟过一轮源码、LW论文、调试文档、讲解视频都仔细过了一遍今天就把整个链路掰开揉碎讲清楚顺便把那些文档里没写明白的坑也补上。这个项目的定位很明确它不是单纯的机票预订系统而是把“预订”和“个性化推荐”结合起来。用户进入系统后除了自己搜航班还能在首页看到基于点击、收藏、下单行为生成的推荐航班列表。技术栈上JavaSSM 负责用户、航班、订单这些核心业务Flask 作为轻量级推荐服务专门跑相似度计算和推荐结果输出。适合正在做Java毕设、想了解推荐系统落地、或者打算把SSM和Python服务整合到同一个项目里的同学参考。1. 需求拆解票务网站为什么要“推荐”而不是只靠搜索1.1 航空票务的典型业务闭环航空票务系统的核心链路并不复杂用户登录、搜索出发地和目的地、查看航班列表、选择航班、下单支付、出票。大部分课程设计做到这里就结束了但这套系统把“推荐”塞进了业务闭环里等于在传统预订流程上多了一个决策入口。推荐到底插在哪一步有两种常见做法。第一种是在首页做“猜你喜欢”区域用户还没搜索时就能看到一批可能感兴趣的航班。第二种是在航班详情页下方做“相似航班推荐”比如用户看了北京到上海的早班机系统推同航线的其他时段、或者邻近城市的替代航线。这套系统两种都做了核心目的不是替代搜索而是提升转化率——用户不知道买哪个航班时给他一个“看起来顺眼”的选项。1.2 推荐系统要解决的核心问题推荐系统看起来高大上落地到航空票务场景无非三个问题冷启动新注册用户没有任何行为数据怎么推荐总不能推空列表。实时性用户刚点击了一个航班下次请求推荐时应该立刻反映出来不能等着离线任务每天跑一次。多样性不能每次打开都推同一个航线用户会烦。针对这三个问题系统的常规解法是冷启动阶段用热门航线按出发城市热度兜底实时性靠Flask端每次请求时动态读取行为表计算多样性在排序时加入时间衰减和随机扰动保证推荐列表不僵死。1.3 系统角色划分游客、注册用户、管理员这套系统有三类角色权限和可见内容都不一样角色核心权限推荐模块表现游客浏览航班、搜索只有热门航班列表不做个性化注册用户搜索、下单、查看订单首页个性化推荐详情页相似推荐管理员管理航班、处理订单、查看统计不涉及推荐但能看到推荐点击效果数据个性化推荐必须登录后才生效否则每次用游客身份点击“猜你喜欢”都返回一样的热门列表就失去推荐意义了。我在实际跑项目时把游客与登录用户的接口分开前端根据登录状态决定调哪个接口这个设计在后边写论文时也好描述——推荐模块不是“可有可无的花架子”而是跟用户体系绑定的功能模块。2. 双后端架构SSM处理业务Flask专职推荐2.1 为什么在SSM之外还要引Flask很多同学看到“JavaSSMFlask”会问一个项目为什么搞两个后端这不是为了凑技术栈而是推荐算法的实现成本问题。SSM里的Java代码写增删改查很顺手但要做相似度计算、矩阵运算、排序融合代码会变得很别扭。而Python有现成的数据处理生态Flask又足够轻把推荐逻辑单独拎出来做成一个HTTP服务两边各干各擅长的。用个生活化的类比SSM像是酒店前台负责登记入住、结账、分配房间这些流程必须稳定可靠Flask像是酒店的旅游顾问你告诉他你上次去了哪里他给你推荐下次去哪玩。前台没必要自己研究旅游路线顾问也没必要管房卡。这个项目里SSM还是绝对核心Flask只暴露两三个推荐相关接口。Spring中的Service通过RestTemplate调用Flask把userId和当前浏览的flightId传过去Flask把计算好的推荐航班ID列表返回SSM再根据ID去数据库查完整航班信息返回给前端。这样设计的好处是如果将来要换推荐算法只改Flask服务不动SSM业务代码。2.2 两个服务之间的通信机制两个Java和Python服务通信最通用的方式就是HTTPJSON。Flask作为被调用方SSM作为调用方。通信的完整链路如下前端请求SSM的/api/recommend?userId1size10。SSM校验登录状态确认 userId 存在。SSM用 RestTemplate 调用 Flask 的http://localhost:5000/recommend?userId1topN10。Flask根据行为数据算TopN航班ID返回{code:0,data:[1001,1002,1003]}。SSM收到ID列表去MySQL查航班表拼装完整VO返回前端。端口、路径、超时时间都要抽到配置文件里别写死在代码中。我当时把Flask地址放在SSM的application.properties中用Value注入方便不同环境切换。调用时设置了connectTimeout2000, readTimeout3000Flask挂了不至于拖垮整个下单流程。2.3 接口设计实例推荐接口的请求与响应Flask端核心接口我写成了这样这是项目里很典型的实现方式from flask import Flask, request, jsonify app Flask(__name__) app.route(/recommend, methods[GET]) def recommend(): user_id request.args.get(userId, typeint) top_n request.args.get(topN, default10, typeint) current_flight request.args.get(flightId, defaultNone, typeint) if user_id is None: return jsonify({code: 1, msg: 缺少userId, data: []}) # 实际逻辑会调用协同过滤计算这里简化演示 rec_ids get_recommend_list(user_id, current_flight, top_n) return jsonify({code: 0, msg: ok, data: rec_ids}) def get_recommend_list(user_id, current_flight, top_n): # 根据用户行为表计算无行为则走热门兜底 return [1001, 1002, 1003]SSM侧用RestTemplate去调public ListInteger fetchRecommendList(Integer userId, Integer flightId, Integer topN) { String url String.format(%s/recommend?userId%dflightId%dtopN%d, flaskBaseUrl, userId, flightId null ? 0 : flightId, topN); ResponseEntityRecommendResponse resp restTemplate.getForEntity(url, RecommendResponse.class); if (resp.getStatusCode().is2xxSuccessful() resp.getBody() ! null) { return resp.getBody().getData(); } return Collections.emptyList(); }接口设计上要注意一点Flask返回的data只是航班ID列表不是完整航班信息。为什么因为航班价格、余票这些动态数据存在MySQL里Flask直接查数据库也行但为了保持推荐服务“只算不算”让它只输出ID再由SSM统一查库组装这样数据一致性更好也不会出现Flask查到的价格和SSM查到的价格不一致的情况。3. 推荐算法的落地从协同过滤到特征融合3.1 冷启动阶段基于航班热度与城市关联的ItemCF用户没有行为数据时我项目里用的策略是“按城市关联航班热度”的组合推荐。具体做法这样取用户注册时填写的常驻城市推荐出发地为该城市的航班中评分最高的5条。如果用户没有填写常驻城市推荐全部航班中点击率最高的TopN。结合当前季节比如7月推热门旅游城市航线12月推南方温暖城市航线。当用户产生点击行为之后再过渡到基于物品的协同过滤ItemCF。ItemCF的核心逻辑是如果用户A点击了北京-上海航班也点击了北京-广州航班那么这两条航班之间有一定相似度另一个用户点击了北京-上海就给他推荐北京-广州。相似度计算通常用余弦相似度sim(i, j) 同时喜欢航班i和航班j的用户数 / sqrt(喜欢i的用户数 * 喜欢j的用户数)这里“喜欢”不是二元概念而是把行为加权比如点击权重1、收藏权重3、下单权重5。算用户对航班的“评分”有了评分再算相似度。3.2 用户行为建模点击、收藏、下单的权重设计行为权重是整个推荐效果的关键。权重设不好会出现两种情况下单权重太高推出来的全是头等舱、国际长途这种高客单价航班用户根本买不起。点击权重太高容易被标题党航班刷屏用户随便点了几个结果推荐全都是相似的。我项目里采用的经验值权重如下行为类型权重说明点击1最轻量的兴趣信号但量大收藏4主动保存意图较强下单8强意图但客单价因素要平滑考虑到机票价格差异大下单后还要把价格归一化。比如用户下单了1200元的航班和500元的航班不能简单认为他对1200元的兴趣是500元的2.4倍。我在代码里对行为分数做了处理价格超过用户历史平均订单价2倍时分数打折低于平均价0.5倍时分数也打折。这么做的目的是避免推荐结果全部偏向高价格航班毕竟用户买低价票大多是因为便宜不是因为特别喜欢那条航线。3.3 Flask端实时推荐接口的相似度计算细节实时性是这个项目的亮点。用户每次点击航班行为表里插入一条记录下一次调Flask推荐接口时就要把最新的行为算进去。Flask端计算流程拆成两步从MySQL查询当前用户最近30天的行为记录。对每条行为记录找出相似航班按相似度加权累加分数排序取TopN。实际上每次请求都全量计算会很慢所以我在Flask里加了一层functools.lru_cache把最近1分钟内的计算结果缓存住参数相同就直接返回缓存。另外还用了Redis做二级缓存用户第一次计算的结果存Redis有效期15分钟。实时计算还要解决一个常见问题用户刚点击了A航班推荐列表里又把A推出来了怎么办这体验很糟。所以在候选集里要排除用户最近已经点击或下单过的航班ID在SQL查询时直接用NOT IN过滤掉不推荐已经看过还下单的航班。4. 数据库与核心表结构设计4.1 用户表、航班表、订单表、行为表数据库设计直接影响推荐逻辑好不好写。这套系统至少需要五张核心表用户表、航班表、订单表、用户行为表、推荐缓存表。我给出关键字段设计可以直接抄CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, city varchar(50) DEFAULT NULL COMMENT 常住城市, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE flight ( id int(11) NOT NULL AUTO_INCREMENT, flight_no varchar(20) NOT NULL COMMENT 航班号, from_city varchar(50) NOT NULL, to_city varchar(50) NOT NULL, departure_time datetime NOT NULL, arrival_time datetime NOT NULL, price decimal(10,2) NOT NULL, remaining_seats int(11) NOT NULL COMMENT 余票, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, flight_id int(11) NOT NULL, order_status tinyint(4) DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user_behavior ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, flight_id int(11) NOT NULL, behavior_type varchar(20) NOT NULL DEFAULT click COMMENT click/favorite/order, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;行为表是推荐系统最重要的数据源索引一定要加在(user_id, create_time)上。我刚开始测试时没加索引数据量到5万条后Flask查询一次行为要2秒多用户体验直接崩塌。加完复合索引之后耗时降到几十毫秒这个细节无论写论文还是实际答辩都值得拿出来讲。4.2 推荐结果缓存表的设计用户每次打开首页都现算推荐会白白消耗资源。我在项目里设计了一张推荐缓存表CREATE TABLE recommend_cache ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, recommend_data text NOT NULL COMMENT 航班ID列表JSON, expire_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次推荐接口被调用时先查这张表没过期就直接返回recommend_data里的JSON过期了才重新计算。缓存过期时间我设成15分钟既能看到行为变化带来的推荐变动又不至于每次都全量计算。要注意的是下单行为发生时要主动清掉这个用户的缓存不然用户都买完票了首页还推同一班航班显得系统不智能。4.3 航班静态信息与动态信息的分离航班表设计时要把“静态信息”和“动态信息”分清楚。航班号、起降城市、起降时间是静态的价格、余票是动态的。实际开发中价格和余票最好单独建一张flight_inventory表和时间相关按日期存储CREATE TABLE flight_inventory ( id int(11) NOT NULL AUTO_INCREMENT, flight_id int(11) NOT NULL, flight_date date NOT NULL, price decimal(10,2) NOT NULL, remaining_seats int(11) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_flight_date (flight_id, flight_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;推荐算法只用静态信息算相似度因为价格每天波动并不影响“北京到上海这条航线跟北京到广州这条航线相似”这个判断。到了下单环节再根据订单里的起飞日期去查flight_inventory拿真实价格和余票。如果不分离推荐模块每次计算都要关联一张可能每秒都在更新的表不仅慢还可能算出已经开始占位但马上要释放余票的航班推荐给用户后却买不了票这就是白推荐了。5. 前后端联调与调试文档里的高频坑5.1 跨域与CORS两个后端都要配如果前端用Vue或普通HTML页面端口是8080SSM是8081Flask是5000那三个服务之间跨域问题几乎必然出现。SSM作为业务后端给前端返回数据时要配跨域。我项目里直接用Spring MVC的CrossOrigin注解也能加全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }Flask端也要允许跨域否则前端一旦直接请求Flask比如某些浏览器调试场景会报错。简单用after_request加响应头app.after_request def after_request(resp): resp.headers[Access-Control-Allow-Origin] * resp.headers[Access-Control-Allow-Headers] * resp.headers[Access-Control-Allow-Methods] GET,POST,PUT,DELETE return resp5.2 Session与登录状态在多服务间的共享SSM里用Session保存登录用户Flask是无状态服务不保存Session。推荐的调用链是前端→SSM→FlaskSSM知道当前登录用户是谁再把userId传给Flask所以Session共享问题只在SSM内部解决Flask不参与。但如果推荐接口被前端直接调用且没有登录校验任何人都能传入一个userId来拿推荐数据这就有越权风险。稳妥做法是Flask监听固定内网IP不对外暴露或者前端请求推荐也先走SSM由SSM代理转发不要直接调Flask。我在调试时踩过一次坑前端改了接口地址直接从浏览器调Flask的/recommend由于没有登录态校验返回了另一个用户的推荐列表。后来我把Flask服务绑定在127.0.0.1:5000只允许本机SSM访问从根源杜绝了外部直连。5.3 中文乱码、时区、日期格式联调时最容易遇到的一类问题就是中文乱码。Java后端连接MySQL的URL要拼上characterEncodingutf8Flask返回JSON时要设置ensure_asciiFalse否则中文会变成\uXXXX的Unicode转义前端显示一大串乱码。另外前后端传日期不要传字符串约定好统一用时间戳或yyyy-MM-dd HH:mm:ss格式的字符串。我项目里直接约定所有接口日期字段返回bigint毫秒时间戳由前端自己格式化省去了时区转换的麻烦。5.4 调试文档应该怎么写才有价值调试文档不是把启动步骤抄一遍就算完。我在整理这套项目的调试文档时按“环境准备→启动顺序→接口自测→常见问题”四层来写。启动顺序尤其重要必须先启动MySQL和Redis再启动Flask最后启动Tomcat上的SSM。因为SSM启动时如果连不上MySQL会直接报错而Flask启动时如果Redis没起很多缓存操作会静默失败表面上不报错实际推荐数据一直拿不到。常见问题部分我建议做成“错误现象→原因分析→解决方案”的表格比如错误现象原因解决方案首页推荐列表空白Flask服务没启动或端口不对检查curl http://localhost:5000/recommend登录后推荐无变化按了游客接口确认前端已切换/api/recommend/personal航班价格显示乱码MySQL连接URL未加utf8追加characterEncodingutf8SSM调用Flask超时Flask计算量过大或缓存失效检查行为表索引、增加Redis缓存调试文档的价值在于别人拿到项目后即使没我讲解也能照着文档一步步把系统跑起来。写的时候多站在“零基础接手人”的角度想问题而不是自己看得懂就行。6. 论文LW撰写与系统演示准备6.1 论文结构怎么对应项目模块毕设论文的结构基本可以和项目模块一一对应。拿到这套系统写LW时我建议的大纲如下绪论写航空票务推荐的研究背景、国内外现状、研究内容。需求分析把三类角色用例画清楚重点描述推荐功能的非功能性需求如响应时间低于1秒、支持并发等。系统设计技术架构图、功能模块划分、数据库ER图。推荐算法详细设计冷启动策略、协同过滤公式、行为权重表。系统实现每个模块的核心代码片段截图尤其要在Flask推荐接口部分贴出相似度计算代码。系统测试测试用例、性能测试结果。写算法部分时不要只贴代码要把公式推导过程写出来。比如ItemCF的余弦相似度公式为什么要做用户惩罚因为一个活跃用户给几百条航班点过赞他对相似度计算的贡献应该低于只看过两条航班的用户。这些细节才是答辩时老师觉得“你真的做了”的关键点。6.2 截图与测试数据准备演示系统时最怕的就是当场数据太干净。如果数据库里只有两三个用户、十条航班、零行为记录推荐页面基本展示不出什么内容场面会很尴尬。我在准备测试数据时造了20个用户、50条航班、500条行为数据保证不同用户登录后能看到明显不同的推荐列表。还要准备两组对照账号一组登录后看首页展示个性化推荐另一组不登录看首页展示热门航班。这样演示时一句话就能体现推荐系统的价值“大家看这两个账号看到的推荐完全不一样因为他们的历史行为不同。”有对比讲解效果才强。6.3 答辩讲解的重点答辩时老师大概率会问这三类问题为什么选双后端架构回答要点SSM适合业务事务管理Flask适合数据处理和推荐算法职责分离算法替换成本低。推荐算法是怎么实现的回答要点先讲冷启动再讲ItemCF最后讲行为权重和时间衰减最好当场在白板上写一下相似度公式。如果数据量一大Flask计算变慢怎么优化回答要点Redis缓存、推荐结果缓存表、行为表加索引、限制行为查询时间范围。还有一个容易被追问的点“你的推荐系统怎么评估好不好”我在论文里用了简单的线下评测把行为数据按时间分成训练集和测试集训练集算推荐列表测试集看用户是否点击了推荐航班算精确率和召回率。虽然指标不够专业但胜在能自圆其说比直接说“我觉得推荐挺准”强得多。最后再说点个人体会这套项目我从零开始跑通最大的感受是双后端架构并没有想象中复杂真正麻烦的是两个服务之间的数据边界划分。刚开始我让Flask直接查数据库结果Flask和SSM各自查一遍数据经常对不上后来改成Flask只管算ID、SSM负责查详情问题立刻减少。这个“职责单一”的设计思路不仅在这套系统里适用以后做任何多服务项目都可以参考。另外调试文档千万别当成应付的东西随便写。我按“错误现象→原因→解决”整理完那份文档后自己后面二次部署时节省了大量回忆时间答辩前快速恢复项目环境也全靠它。如果你也打算做类似的毕设建议从第一天就开始记录踩坑日志最后整理成文档这份资料比代码本身更能体现你的工程能力。
返回列表