资讯详情

Java 项目实战: 交友项目 - 今日佳人:需求分析

📅 2026/10/23 9:16:01 | 华诺云谱 👁 阅读
Java 项目实战: 交友项目 - 今日佳人:需求分析
概述「今日佳人」是交友项目 App 首页的第一个推荐位这里只解决两件事推荐数据从哪来、服务端接口怎么设计。CRUD 语法已在前面几篇覆盖这里不再重复需要时回看SpringData-MongoCRUD操作即可。纲要业务背景首页推荐位的产品定位数据来源推荐系统Spark 算法服务写、业务服务读的解耦架构场景选型为什么这个业务落在 MongoDB数据模型recommend_user集合字段与 userId/toUserId 的方向语义接口设计GET /mf/todayBest的入参、返回与「今日」语义调用链路App → 鉴权 → Dubbo → Mongo → 跨存储组装的时序原型还原交友页「今日佳人」卡片线框图异常与边界冷启动、当天无数据、同分排序验证路径mongosh 造数 → 调接口核对业务背景用户登录成功后进入首页首页从上到下大致是三块今日佳人、推荐好友列表、项目/搜附近等功能入口。今日佳人位于页面最顶部产品语义很直白系统每天替当前用户挑选一位「缘分值」最高的异性做成一张卡片推到首页。卡片上展示头像、昵称、年龄、标签和缘分值用户可以点「喜欢」也可以点进去查看详细资料。缘分值在推荐领域叫「打分」或「评分」只是面向用户时包装成了更容易理解的词。它的计算依据是双方的行为数据你给别人点赞、评论、喜欢、查看资料都会累计分数你自己的资料婚姻状态、学历等也参与打分。这些维度汇总后就得到一对用户之间的缘分值。对这个功能来说打分不是服务端的事。项目的整体架构里除了 Java 服务端还有一个独立的推荐系统基于 Spark 的算法服务它负责跑算法、算分然后把结果写进 MongoDB。Java 服务端拿到需求后要做的只有一件事从 MongoDB 里把推荐给当前用户的、缘分值最高的那条记录查出来展示到手机端。这是这里最重要的架构认知值得单独画出来高频批量写入推荐结果按 toUserId 查询 top1用户行为数据点赞/评论/查看资料推荐系统Spark 算法服务MongoDBrecommend_userJava 服务端mf-dubbo-mongoApp 首页今日佳人卡片算法与业务通过 MongoDB 这张「结果表」解耦算法侧不用关心业务接口长什么样业务侧也不用懂推荐算法。哪天算法换模型、换打分公式服务端一行代码不用改只要写入的集合结构不变。后面的推荐系统章节会展开算法侧的实现这里始终站在「读结果」这一侧。为什么这个场景用 MongoDB选型论证在引入篇已经完整讲过这里只对号入座看这个场景的三个特征正好落在 MongoDB 的强项上场景特征MongoDB 的对应能力推荐结果由算法服务高频、批量写入每天为每个用户生成一批推荐记录文档模型写入成本低批量 insert 无需维护严格的表结构约束天然适合「算完就写」的结果落库查询模式固定按toUserId点查按 score 排序取 topN单集合点查 排序是 MongoDB 最基础的查询路径配合索引性能足够数据有时效性每天的推荐结果只对当天有意义旧数据可整体清理无 Schema 约束、按日期字段批量删除成本低历史数据不拖累在线查询一句话这是典型的「结果存储」场景数据是别的系统算出来的产物业务只做读写MongoDB 的文档模型和写入吞吐正好匹配。数据模型recommend_user集合一共五个字段字段不多但方向语义是全篇最容易写错的地方。字段类型说明_idObjectId主键MongoDB 自动生成userIdLong被推荐人的用户 id佳人/帅哥是谁toUserIdLong目标用户的用户 id推荐给谁即当前登录用户scoreDouble推荐得分页面上展示为「缘分值」dateString推荐日期如2026/9/30用于「今日」过滤与旧数据清理对应实体类实读自mf-model模块packagecom.mf.model.mongo;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.bson.types.ObjectId;importorg.springframework.data.mongodb.core.mapping.Document;AllArgsConstructorNoArgsConstructorDataDocument(collectionrecommend_user)publicclassRecommendUserimplementsjava.io.Serializable{privateObjectIdid;//主键idprivateLonguserId;//推荐的用户idprivateLongtoUserId;//用户idprivateDoublescore0d;//推荐得分privateStringdate;//日期}注意集合里只存用户 id昵称、头像这些资料一概不存它们在 MySQL 的user_info表里展示时通过 id 关联查询。推荐结果这种高频重写的数据没必要在 Mongo 里冗余一份用户资料。userId 与 toUserId 的方向两个字段长得很像方向搞反查询结果就全错了。用一句话锚定userId 是「推荐出来的那个人」toUserId 是「看卡片的那个人」。两个方向的查询语义完全不同查询目标条件业务含义给 userId 推荐了谁where(toUserId).is(userId)当前用户首页要展示的佳人列表今日佳人走这个方向userId 被谁推荐where(userId).is(userId)「我出现在多少人的推荐页上」反向运营/排查时用假设当前登录用户是 106 号推荐系统给他算了一批结果落库大概是这样use mf// 106 是目标用户toUserId每次推荐的佳人是 userIddb.recommend_user.insertMany([{userId:NumberLong(1),toUserId:NumberLong(106),score:89,date:2026/9/30},{userId:NumberLong(1024),toUserId:NumberLong(106),score:95,date:2026/9/30},{userId:NumberLong(2048),toUserId:NumberLong(106),score:72,date:2026/9/30}])// 验证按 toUserId 查 106 的推荐score 降序取第一条期望拿到 userId1024、score95db.recommend_user.find({toUserId:NumberLong(106)}).sort({score:-1}).limit(1)写代码前先在 mongosh 里把这条查询跑通后面 Java 侧的Criteria.where(toUserId)不过是把同样的条件翻译过来。接口设计实读工程代码本节只交付一个接口首页卡片的 top1 查询。推荐好友列表是后面的章节这里不涉及。项目内容路径GET /mf/todayBest请求参数无 URL 参数用户身份从请求头 token 解析拦截器写入 ThreadLocal返回ResponseEntity包装的TodayBestVO今日缘分值最高的一个用户所属模块mf-app-server消费者TodayBestVO 的字段实读自mf-modelpackagecom.mf.model.vo;importcom.mf.model.domain.UserInfo;importcom.mf.model.mongo.RecommendUser;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.beans.BeanUtils;/** * 今日佳人 */DataNoArgsConstructorAllArgsConstructorpublicclassTodayBest{privateLongid;//用户idprivateStringavatar;privateStringnickname;privateStringgender;//性别 man womanprivateIntegerage;privateString[]tags;privateLongfateValue;//缘分值/** * 在vo对象中补充一个工具方法封装转化过程 */publicstaticTodayBestinit(UserInfouserInfo,RecommendUserrecommendUser){TodayBestvonewTodayBest();BeanUtils.copyProperties(userInfo,vo);if(userInfo.getTags()!null){vo.setTags(userInfo.getTags().split(,));}vo.setFateValue(recommendUser.getScore()null?0L:recommendUser.getScore().longValue());returnvo;}}接口返回的 JSON 大致长这样fateValue就是卡片上的「缘分值89」{id:1024,avatar:https://mf.example.com/avatar/1024.png,nickname:小美,gender:woman,age:22,tags:[单身,本科,年龄相仿],fateValue:95}有两个设计点值得展开。跨存储组装。recommend_user里只有 id 和 score接口返回的昵称、头像、年龄来自 MySQL 的UserInfo。所以 Service 层查到RecommendUser后要拿recommendUser.getUserId()再调userInfoApi.findById()去查用户资料最后在TodayBest.init()里用BeanUtils.copyProperties拼成一个 VO。这个「Mongo 查关系、MySQL 查资料、内存拼 VO」的模式和黑名单分页里先批查 Mongo 再补用户信息的做法是同一个思路后面凡是有推荐性质的功能都会反复出现。「今日」的语义怎么实现。讲义的表结构里设计了date字段但实读RecommendUserApiImpl的查询条件只有toUserId等值匹配 score 降序 limit(1)并没有按 date 过滤当天DubboServicepublicclassRecommendUserApiImplimplementsRecommendUserApi{AutowiredprivateMongoTemplatemongoTemplate;//查询今日佳人publicRecommendUserqueryWithMaxScore(LongtoUserId){//根据toUserId查询根据评分score排序获取第一条//构建CriteriaCriteriacriteriaCriteria.where(toUserId).is(toUserId);//构建Query对象QueryqueryQuery.query(criteria).with(Sort.by(Sort.Order.desc(score))).limit(1);//调用mongoTemplate查询returnmongoTemplate.findOne(query,RecommendUser.class);}}这是一个教学工程的简化date字段保留在模型里「当天」由数据侧保证推荐系统每天重写数据、旧数据定期清理集合里始终只有最新一天的结果查询自然就是「今日」的。生产上更稳妥的做法是查询条件里加Criteria.where(date).is(今天)或者按 date 降序先锁定最新一天再取 top1两种方案的选择依据在「异常与边界」一节讨论。调用链路把整条链路串起来注意分层的归属controller、service 在消费者mf-app-server服务提供者是mf-dubbo-mongo模块它没有 DAO/DO 层直接是 API 层通过MongoTemplate操作 Mongo。MySQL user_infoUserInfoApi(Dubbo)MongoDBMongoTemplateRecommendUserApi(Dubbo)mfServicemfControllerApp 首页MySQL user_infoUserInfoApi(Dubbo)MongoDBMongoTemplateRecommendUserApi(Dubbo)mfServicemfControllerApp 首页拦截器校验 tokenUserHolder.getUserId()GET /mf/todayBest (携带 token)todayBest()queryWithMaxScore(userId)Query(toUserIduserId).sort(score desc).limit(1)findOneRecommendUser(userId, score)RecommendUserRecommendUserfindById(recommendUser.getUserId())查询用户资料UserInfo(昵称/头像/年龄/标签)UserInfoTodayBest.init(userInfo, recommendUser)TodayBest200 OK TodayBest JSON数据从两套存储各取一半在 service 层合成一个 VO 返回这就是前面说的跨存储组装落在链路上的位置。原型还原打开原型文件交友.html 对应的页面文本首页「今日佳人」卡片区域的可见文案有今日佳人、缘分值、缘分值89、小美、单身 / 本科 / 年龄相仿、喜欢、聊一下。还原成线框图-------------------------------------- | 项目 [推荐] | | | | -------------------------------- | | | 今日佳人 | | | | ------------------------------ | | | | | | | | (用户头像) | | | | | | | | 小美 | | | | 单身 本科 年龄相仿 | | | | | | | | 缘分值89 | | | | | | | | [ 喜欢 ] [ 聊一下 ] | | | -------------------------------- | | | | (向下滑动推荐好友列表...) | | | -------------------------------------- | 交友 | 消息 | 我的 | | --------------------------------------要点「今日佳人」是首页顶部的独立卡片区域一屏一位数据来自GET /mf/todayBest。卡片信息自上而下头像、昵称、标签组单身/本科/年龄相仿来自UserInfo.tags拆分、缘分值来自fateValue原型示例为 89。操作按钮「喜欢/聊一下」对应后续的喜欢与 IM 功能这里只负责把卡片数据展示出来。底部 Tab 为交友/消息/我的三个主入口卡片属于「交友」页。向下滑动后今日佳人卡片缩小消失让位给推荐好友列表原型注释原文。异常与边界边界情况现象实读代码的取舍生产取舍建议新用户无推荐数据冷启动queryWithMaxScore返回 null兜底new RecommendUser()塞入userId1, score99展示系统默认用户接口不报错、页面不空白可接受更精细的做法是返回兜底推荐位或引导用户完善资料但首版「给个默认人」成本最低当天无数据推荐系统故障集合里是昨天的数据实读代码不过滤 date昨天的 top1 会被返回等于天然降级为「查最近一天」若改为按 date 过滤就要配套「查不到时回退最近一天」的降级查询否则当天页面直接开天窗两种策略二选一别混用score 相同的多条记录排序不确定Mongo 的 sort 对同分文档不保证稳定顺序limit(1)每次可能返回不同人补第二排序键date或_idSort.by(desc(score), desc(_id))让结果确定或由算法侧保证同分不打顺带说明recommendUser null时兜底造的假数据userId1要求user_info表里真的存在 id1 的用户否则下一步userInfoApi.findById(1)查出 nullTodayBest.init里的copyProperties会抛 NPE。造验证数据时记得连这个兜底用户一起造。验证要点需求分析阶段能落地的验证路径是「先造数、再对答案」# 1. mongosh 造数用上文数据模型一节的 insertMany注意 toUserId 用当前登录用户 id# 2. 起 mf-dubbo-mongo端口 18082与 mf-app-server# 3. 带登录 token 调接口curl-HAuthorization: tokenhttp://localhost:8080/mf/todayBest核对三点返回的id是否为该toUserId下 score 最大的那条记录对应的userIdfateValue是否等于该条记录的scoreDouble 转 Long小数部分被截断把同toUserId的记录调换 score 再查一次返回应跟着变化确认查询方向没写反。常见误解澄清误解事实缘分值是 Java 服务端算的不是。打分由独立的 Spark 推荐系统完成并写入 Mongo服务端只读结果userId是当前登录用户反了。toUserId才是当前登录用户userId是被推荐的佳人接口需要传日期参数来限定「今日」不需要。接口无 URL 参数「今日」由数据侧每天重写/清理保证实读代码未按 date 过滤recommend_user里存了佳人的昵称头像只存 id 和 score资料在 MySQL展示时跨存储组装查到 null 应该直接返回空对象给前端实读代码造了一条userId1, score99的兜底数据页面永远有卡片总结今日佳人 首页推荐位 读推荐结果里缘分值最高的一个人算分是推荐系统Spark的事写入recommend_user集合Java 服务端只消费算法与业务靠一张 Mongo 集合解耦。方向语义记住一句话toUserId是看卡片的人userId是卡片上的人今日佳人按toUserId查、按 score 降序取第一条。接口GET /mf/todayBest无参数、返回单个TodayBest昵称头像要从 MySQL 补齐TodayBest.init()负责拼装。边界上抓住三件事冷启动兜底、当天无数据的降级策略二选一、同分排序补第二排序键。下一篇是同章的今日佳人编写服务提供者搭mf-dubbo-mongo模块启动类、application.yml、RecommendUserApi 与实现类把这里定义的查询真正跑起来。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑