概述
左滑「不喜欢」和右滑「喜欢」代码结构几乎一样,但产品语义完全不同:右滑可能触发配对,左滑只负责把人从推荐里排出去。本文讲清这个不对称性带来的实现差异,以及「不感兴趣」对推荐列表的持久影响。
纲要
- 左滑与右滑的产品不对称性:为什么左滑不做配对检查
- 接口契约:api/109(
GET /mf/{id}/unlove) - 实现总览:
notLikeUser与likeUser的镜像对照(链路细节见「左滑右滑实现」相关文章) - 核心增量:「不感兴趣」的推荐影响,
queryCardsList的NIN排除与反悔机制 - 时序图、踩坑清单、验证步骤## 左滑与右滑的产品不对称
项目推荐页里,右滑是强动作:我喜欢的人,对方也喜欢我,系统要立刻建好友关系(项目好友表 + 环信)。左滑是弱动作:它不是「拒绝这个人」,而是给推荐系统一条反馈信号,「这个方向不对,别再推了」。
这个语义差异直接决定了代码差异:
| 维度 | 右滑 likeUser | 左滑 notLikeUser |
|---|---|---|
| Mongo 存储 | saveOrUpdate(userId, likeUserId, true) | saveOrUpdate(userId, likeUserId, false) |
| Redis 操作 | SADD LIKE,SREM NOT_LIKE | SADD NOT_LIKE,SREM LIKE |
| 配对检查 | 有,双向喜欢则加好友 | 没有 |
| 计数 | 项目页有喜欢计数 | 无计数 |
左滑为什么不做配对检查?倒推一下就明白:推荐列表(queryCardsList)在查询时已经排除了所有喜欢过和不喜欢的用户,对方根本不会出现在你的左滑列表里;同时你也不在对方的排除名单之外,换句话说,能被你左滑的人,必然是「你还没表过态」的人,不存在「你们已经是好友再左滑」的正常路径。所以「双向喜欢则取消好友关系」这一步在当前系统里走不到,本文将其留作自行实现的练习(真要做,需同时删项目好友表和环信好友数据)。
有个细节别漏:notLikeUser里 SREM LIKE 这一行不能省。用户可能先右滑了某人,后来反悔左滑,这时 Mongo 里的态度会翻转,但 Redis 的 LIKE 集合如果不删,配对判断isLike(基于 Redis)仍会认为双方互相喜欢。Mongo 和 Redis 必须一起翻转,这是和likeUser完全镜像的操作。
接口契约(api/109)
YAPI 接口编号 109,请求路径与右滑的love相同,仅动作后缀不同:
| 项目 | 内容 |
|---|---|
| 接口路径 | GET /mf/{id}/unlove |
| 请求方式 | GET |
| 路径参数 | id,被左滑的用户 ID |
| 请求体 | 无 |
| 响应 | ResponseEntity<Void>,200 即成功 |
// 请求示例GET/mf/3/unlove// 响应:空 body,状态码 200控制器实现只做转发,实读mfController:
/** * 不喜欢 */@GetMapping("{id}/unlove")publicResponseEntity<Void>notLikeUser(@PathVariable("id")LonglikeUserId){this.mfService.notLikeUser(likeUserId);returnResponseEntity.ok(null);}实现总览:与 likeUser 镜像
mfService.notLikeUser的链路结构和likeUser一模一样,只有两处参数相反(完整逐行讲解见「左滑右滑实现」相关文章,这里给总览):
// 不喜欢 —— 结构与 likeUser 镜像publicvoidnotLikeUser(LonglikeUserId){//1、调用API,保存不喜欢数据到MongoDB(isLike = false)Booleansave=userLikeApi.saveOrUpdate(UserHolder.getUserId(),likeUserId,false);if(!save){thrownewBusinessException(ErrorResult.error());}//2、操作redis:写入不喜欢集合,删除喜欢集合(与 likeUser 恰好相反)redisTemplate.opsForSet().add(Constants.USER_NOT_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());redisTemplate.opsForSet().remove(Constants.USER_LIKE_KEY+UserHolder.getUserId(),likeUserId.toString());//3、判断双向喜欢则删除好友 —— 当前推荐逻辑下走不到,留作练习}Redis key 常量在Constants中,值实读为"USER_NOT_LIKE_SET_",按用户 ID 拼接成每人一个 Set:
//用户不喜欢Redis的keypublicstaticfinalStringUSER_NOT_LIKE_KEY="USER_NOT_LIKE_SET_";「不感兴趣」的推荐影响:NIN 排除
这是左滑真正的产品价值所在。推荐卡片列表的查询在RecommendUserApiImpl.queryCardsList中,先查出当前用户在user_like表里的全部表态对象,再用nin从推荐池里剔除:
publicList<RecommendUser>queryCardsList(LonguserId,intcounts){//1、查询喜欢不喜欢的用户IDList<UserLike>likeList=mongoTemplate.find(Query.query(Criteria.where("userId").is(userId)),UserLike.class);List<Long>likeUserIdS=CollUtil.getFieldValues(likeList,"likeUserId",Long.class);//2、构造查询推荐用户的条件Criteriacriteria=Criteria.where("toUserId").is(userId).and("userId").nin(likeUserIdS);//3、随机获取推荐的用户列表TypedAggregation<RecommendUser>newAggregation=TypedAggregation.newAggregation(RecommendUser.class,Aggregation.match(criteria),Aggregation.sample(counts));AggregationResults<RecommendUser>results=mongoTemplate.aggregate(newAggregation,RecommendUser.class);returnresults.getMappedResults();}注意这里查user_like时没有过滤isLike,不管你右滑过还是左滑过,都进排除名单。所以左滑的效果是持久的:被你左滑的人,从此永远不再出现在你的推荐列表里,直到你改变态度。这就是「不感兴趣」的产品语义:一次左滑 = 长期排除,而不是本轮跳过。
反悔怎么办?答案是再滑一次。UserLikeApiImpl.saveOrUpdate按(userId, likeUserId)查记录,存在就只更新isLike字段,同一条记录承载两种态度,左滑变右滑时记录被原地翻转为isLike=true,NIN名单随表态变化自动更新,这个人就重新回到推荐池。这个设计在「数据建模」相关文章中(user_like一条记录两种态度)已经铺垫过,左滑场景正是它发挥价值的地方:不需要删记录再重建,态度翻转零成本。具体的「左滑后想右滑」反悔流程,「左滑右滑实现」相关文章已有完整讲解,此处不重复。
反悔怎么办?答案是再滑一次。UserLikeApiImpl.saveOrUpdate按(userId, likeUserId)查记录,存在就只更新isLike字段,同一条记录承载两种态度,左滑变右滑时记录被原地翻转为isLike=true,NIN名单随表态变化自动更新,这个人就重新回到推荐池。这个设计在数据建模篇(user_like一条记录两种态度)已经铺垫过,左滑场景正是它发挥价值的地方:不需要删记录再重建,态度翻转零成本。具体的「左滑后想右滑」反悔流程,左滑右滑实现篇已有完整讲解,此处不重复。
时序图
常见误解澄清
| 误解 | 实际情况 |
|---|---|
| 左滑也该判断双向喜欢,取消好友 | 推荐查询已排除表态过的用户,正常流程左滑不到好友;该步骤留作练习 |
| 左滑只是「这轮跳过」,下次还会推荐 | NIN按user_like全量排除,左滑是长期排除 |
| 反悔左滑要删数据重新右滑 | 翻转同一条记录的isLike即可,记录不删 |
| 排除名单存在 Redis 里 | queryCardsList读的是 Mongo 的user_like,Redis Set 只服务于配对判断 |
左滑接口路径是/mf/{id}/love | 机翻稿此处转写有误,实读代码与 api/109 均为{id}/unlove |
踩坑清单
- 照抄 likeUser 带上配对检查:最常见的手滑。左滑调用
messagesService.contacts会导致「我讨厌的人成了我好友」级别的逻辑事故。 - 忘了 SREM LIKE:只 SADD NOT_LIKE 不删 LIKE,先右滑后左滑的用户在 Redis 里两边都有,
isLike配对判断失真。 - Mongo 与 Redis 态度不一致:两步操作无事务保护,失败路径要抛
BusinessException让前端感知,别吞异常。 - 产品层面没有反悔入口:
NIN排除是永久性的,客户端如果不提供「重新推荐」或历史列表入口,用户误滑后无解,这是产品设计要提前想清楚的点。
验证
按课程实测路径走一遍(106 号用户):
- 登录 106 号用户,进入项目推荐页,连续左滑若干人。
- 查 Mongo
user_like集合:出现userId=106, isLike=false的记录。 - 查 Redis:
USER_NOT_LIKE_SET_106中有被左滑的用户 ID,SMEMBERS可确认。 - 退出重进项目页:推荐列表人数变少,被左滑的人不再出现。
- (反悔验证)对该用户再右滑:
user_like记录isLike翻转为true,推荐列表恢复出现此人。
总结
- 左滑是推荐反馈信号,不是社交拒绝动作;右滑强(可能配对建好友),左滑弱(只影响推荐),这个不对称性解释了为什么
notLikeUser不做配对检查。 - 实现上与
likeUser完全镜像:MongosaveOrUpdate(isLike=false)+ Redis SADD NOT_LIKE / SREM LIKE,链路细节见左滑右滑实现篇。 - 左滑的核心增量在推荐侧:
queryCardsList用NIN排除user_like全部表态记录,左滑 = 永久排除;反悔靠态度翻转更新isLike,一条记录两种态度的设计在此兑现价值。 - 至此项目模块的喜欢/不喜欢/配对链路全部走通,本章收尾内容见同目录总结和作业。