Java 项目实战:交友项目 - 联系人管理需求分析

发布时间:2026/10/30 6:22:06
Java 项目实战:交友项目 - 联系人管理需求分析 Java 项目实战交友项目 - 联系人管理需求分析上篇完成了用户体系集成任何用环信做 IM 的项目都要走那套流程。接下来就是具体业务了联系人管理好友管理。这里先把需求拆清楚添加好友分几步、联系人列表的数据从哪来、未读消息数到底谁负责统计。接口和代码留到下一篇细说。纲要业务场景IM 的联系人页 好友列表 会话入口一份社交关系两处用需求拆解发送好友申请 → 同意添加 → 查看好友共三件事数据模型friend集合双向两条记录的设计未读消息数服务端不管客户端环信 SDK 本地维护附两种方案对比接口设计GET/POST /messages/contacts的路径、参数、返回核心流程添加好友与查询联系人列表的时序图原型线框消息页与选择联系人页异常边界与验证要点业务场景为什么 IM 页面里会有「联系人」打开交友 App 底部有「交友 / 消息 / 我的」三个 tab用户切到「消息」tab第一屏是会话列表点「选择联系人」就进入通讯录这里列出的就是他的全部好友。用户从这里找到要聊天的人点一下进入聊天页。联系人和社交好友的关系值得单独说一句。在很多产品里这是两套体系关注/粉丝是社交关系IM 联系人是通信录两者可以不一致。项目不是这样一份好友关系两处使用社交侧好友是推荐、互动、圈子互动的深化结果IM 侧好友同时是环信里的联系人直接发起单聊所以加好友时服务端要做两件事把关系写进 MongoDB社交侧可查同时调用环信接口注册好友关系IM 侧能发消息。这两步缺一不可后面讲异常边界时会回到这个「双写」带来的对账问题。需求拆解三件事原型演示的流程走一遍用户 A 在推荐里看到用户 B点「聊一下」查看 B 设置的陌生人问题填好答案提交请求发到项目的 Java 服务器服务器调环信把这条消息推到 B 的手机上。B 看到的是一条「陌生人消息」上面有 A 的问题和 B 自己的答案B 点击「确认添加」两人结为好友。之后 B 打开消息 tab → 通讯录就能看到刚添加的 A点进去开始在线聊天。把流程摊开联系人管理本质上是三项功能功能触发动作服务端动作发送好友申请A 填答案点「聊一下」组装消息体调环信推送给 B同意并添加好友B 点「确认添加」环信 addContact MongoDB 写好友关系双写查看好友用户打开通讯录分页查好友列表组装用户资料返回除此之外还要补齐过程中缺的接口比如点开某个用户时的详情页查看用户信息这些在上一节的 §5.2-5.4 已经覆盖。逻辑都清晰代码量不大难的是想清楚数据怎么组装。数据模型friend 集合是双向两条记录好友关系存在 MongoDB 的friend集合里注意它是 Mongo collection不是 MySQL 的表。关键设计一条好友关系存两条记录A 和 B 互为好友就写(userIdA, friendIdB)和(userIdB, friendIdA)两条。查询时永远只按userId 当前用户去查不需要or条件。packagecom.mf.model.mongo;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.bson.types.ObjectId;importorg.springframework.data.mongodb.core.mapping.Document;/** * 好友表:好友关系表 */DataNoArgsConstructorAllArgsConstructorDocument(collectionfriend)publicclassFriendimplementsjava.io.Serializable{privatestaticfinallongserialVersionUID6003135946820874230L;privateObjectIdid;privateLonguserId;//用户idprivateLongfriendId;//好友idprivateLongcreated;//时间}为什么存两条而不是一条记录配一个or查询好友列表是高频读场景双向两条记录让查询条件退化为单字段等值匹配配合索引就是最简单的查询路径写入时多写一条属于用写成本换读成本对这种读多写少的关系数据是划算的。写入时先exists判重再save防止重复添加产生重复记录。存储位置上项目的工程结构是三层app-server 收 HTTP 请求Dubbo 调用落到 dubbo 模块操作存储。联系人相关的类分布如下mf ├── mf-app-server # 接口层 │ └── com/mf/server │ ├── controller/MessagesController.java # /messages/contacts │ └── service/MessagesService.java # 组装逻辑 ├── mf-dubbo │ ├── mf-dubbo-interface │ │ └── com/mf/dubbo/api/FriendApi.java # save / findByUserId │ └── mf-dubbo-mongo │ └── com/mf/dubbo/api/FriendApiImpl.java └── mf-model └── com/mf/model ├── mongo/Friend.java # 好友关系 collection └── vo/ContactVo.java # 联系人返回 VO未读消息数服务端不统计客户端自己算联系人列表页要显示「未读消息数」小红点这个数从哪来通读本项目的工程代码结论很明确服务端完全没有未读数的逻辑。ContactVo里只有 id、userId、avatar、nickname、gender、age、city 七个字段没有任何未读计数HuanXinTemplate只封装了四个方法createUser、addContact、deleteContact、sendMsg也不涉及离线或未读查询。这不是偷懒环信的官方能力就是这么设计的未读数由客户端 SDK 在本地维护。环信的 iOS/Android SDK 里每个会话EMConversation自带unreadCount属性消息收发过程中 SDK 自动增减App 刷新联系人页时直接读本地数据库渲染。服务端 REST API 虽然能查到离线消息数但项目选择了不走这条路。两种方案对比方案做法优点缺点项目的选择客户端本地统计环信 SDK 维护每会话 unreadCountApp 端直接读服务端零开发实时性好SDK 原生支持换设备后本地计数丢失需拉漫游消息重算多端未读数不同步采用服务端统计自建存储记录每对用户的已读位点或调环信 REST API 查离线数多端一致换设备不丢要维护已读位点的存储与更新或依赖环信接口有调用限额未采用理解这个边界对排查问题很关键如果用户反馈「未读小红点不准」问题大概率在客户端 SDK 的本地数据而不是服务端接口服务端压根没参与未读数计算。服务端的联系人列表接口返回的数据里连未读字段都没有。跨存储组装好友 id → 用户资料联系人列表的展示数据要靠一次跨存储组装friend集合里只有 id 没有昵称头像需要拿 friendId 批量去查用户资料再拼成ContactVo。和推荐列表、圈子列表的做法一脉相承关系数据存一处展示资料存另一处服务层用一次批量查询把它们拼起来。这个套路在项目里已经反复出现熟了之后看接口就知道它内部怎么走。接口设计两个接口共用/messages/contacts路径靠 HTTP 方法区分。接口方法路径参数说明添加好友POST/messages/contactsbody{userId: 1}双写环信 addContact Mongo 存关系联系人列表GET/messages/contactspage、pagesize、keyword昵称搜索可省略分页查好友列表联系人列表的返回结构PageResult包裹ContactVo列表{counts:2,pagesizes:10,pages:1,page:1,items:[{id:106,userId:hx106,avatar:https://mf.com/106.jpg,nickname:小妹,gender:woman,age:18,city:上海},{id:2,userId:hx2,avatar:https://mf.com/2.jpg,nickname:小刚,gender:man,age:22,city:上海}]}userId字段返回的是环信 IDhx 用户id前缀客户端拿它直接对接环信 SDK 发起会话接口设计时就考虑到了下一跳的用法。核心流程添加好友双写MongoDB环信服务器交友服务端用户B客户端MongoDB环信服务器交友服务端用户B客户端前置A 填答案后服务端已通过环信把陌聊消息推给 Balt[环信注册成功][环信注册失败]POST /messages/contacts {userId: A}addContact(hxA, hxB)成功/失败保存 (A,B) 与 (B,A) 两条记录OK200结为好友抛 BusinessException不写库顺序是「先环信、后 Mongo」环信那边失败就抛异常直接结束Mongo 里不会留下环信侧不存在的孤儿好友关系。反过来先写库后调环信一旦环信调用失败本地就多了一条「看起来是好友、实际聊不了」的脏数据。当然这个顺序也有代价环信成功后如果写 Mongo 失败环信侧会多一份好友关系这是双写固有的不一致窗口靠下一篇接口实现里的判重逻辑和重新添加来兜底。查询联系人列表渲染错误:Mermaid 渲染失败: Parse error on line 10: ...S: ListFriend S-S: CollUti -----------------------^ Expecting (), SOLID_OPEN_ARROW, DOTTED_OPEN_ARROW, SOLID_ARROW, SOLID_ARROW_TOP, SOLID_ARROW_BOTTOM, STICK_ARROW_TOP, STICK_ARROW_BOTTOM, SOLID_ARROW_TOP_DOTTED, SOLID_ARROW_BOTTOM_DOTTED, STICK_ARROW_TOP_DOTTED, STICK_ARROW_BOTTOM_DOTTED, SOLID_ARROW_TOP_REVERSE, SOLID_ARROW_BOTTOM_REVERSE, STICK_ARROW_TOP_REVERSE, STICK_ARROW_BOTTOM_REVERSE, SOLID_ARROW_TOP_REVERSE_DOTTED, SOLID_ARROW_BOTTOM_REVERSE_DOTTED, STICK_ARROW_TOP_REVERSE_DOTTED, STICK_ARROW_BOTTOM_REVERSE_DOTTED, BIDIRECTIONAL_SOLID_ARROW, DOTTED_ARROW, BIDIRECTIONAL_DOTTED_ARROW, SOLID_CROSS, DOTTED_CROSS, SOLID_POINT, DOTTED_POINT, got NEWLINE排序是好友建立时间created倒序不是「最近联系」排序ContactVo 里没有 lastMessage、lastTime 这类字段会话时间在客户端环信 SDK 的会话列表里有服务端通讯录接口不掺和。keyword 搜索走的是用户资料查询时的昵称条件。原型线框消息 tab 首屏会话列表┌──────────────────────────────────┐ │ 消息 11:21 │ ├──────────────────────────────────┤ │ [头像] 小刚 │ │ java视频给我发一份 │ │ 11:21 │ ├──────────────────────────────────┤ │ [头像] 大鹏 │ │ 布置的作业你做了么 │ │ 昨天 │ ├──────────────────────────────────┤ │ [头像] 小美 │ │ 程序员报名中 │ │ 2018-01-02 │ ├──────────────────────────────────┤ │ 交友 [消息] 我的 │ └──────────────────────────────────┘要点时间显示规则当天之内显示时间11:21前一天的信息显示「昨天」更早的显示具体日期2018-01-02用户左滑可删除信息未读数小红点由客户端环信 SDK 本地维护服务端接口不含该数据点「选择联系人」进入通讯录页┌──────────────────────────────────┐ │ 返回 选择联系人 │ ├──────────────────────────────────┤ │ 搜索用户 │ ├──────────────────────────────────┤ │ 你关注的用户 │ ├──────────────────────────────────┤ │ [头像] 小美 上海 │ │ [头像] 小刚 上海 │ │ [头像] 大鹏 上海 │ │ [头像] 更多 │ ├──────────────────────────────────┤ │ 交友 [消息] 我的 │ └──────────────────────────────────┘要点顶部搜索框对应接口的keyword参数按昵称搜列表项点击后进入聊天页面头像点击进入用户详情页「更多」点击出弹框拉黑、举报、查看聊天消息点击返回后回到消息界面异常与边界场景服务端行为取舍说明无好友空列表CollUtil.isEmpty判断后直接返回空PageResult不查用户资料省一次跨服务调用好友资料已注销map.get(friendId)为 null 时跳过不进 VO 列表列表静默少一条而非报错潜在副作用是分页条数与实际展示数可能对不上未读数获取失败服务端不处理不参与未读数客户端读 SDK 本地计数失败时降级为不显示未读数是纯展示性数据取不到就藏起小红点不值得为它报错打断列表加载环信成功、Mongo 失败抛异常给用户环信侧残留好友关系双写不一致窗口用户重新添加时 Mongo 判重 环信幂等重试即可修复环信失败、Mongo 未写直接抛BusinessException本地无脏数据顺序保证先环信后 Mongo环信与本地好友不一致以friend集合为准展示通讯录环信关系只影响能否发消息通讯录列表读的是本地库即使环信侧关系丢失列表照常显示聊天发起时才会暴露对账可做定期比对环信 roster 与本地集合项目未实现属于已知简化删好友的场景顺带说一句HuanXinTemplate里有deleteContact方法对应环信service.contact().remove()真正的好友解除要把环信关系和 Mongo 两条记录一起删任何一边只删一半都会复现上面的一致性问题。验证要点加好友后双方通讯录都出现对方验证 Mongo 双向两条记录环信后台查看 roster确认好友关系已在环信侧注册列表分页好友数超过 pagesize 时翻页正确keyword 按昵称过滤生效未读数A 给 B 发消息B 的会话红点 1B 打开会话后归零均为客户端 SDK 行为删除好友在环信控制台删除好友后本地通讯录列表仍然显示该用户数据在 Mongo但发起聊天会因环信关系不存在而失败验证时别把这当 bug这正是双存储各自独立的体现换设备登录联系人列表完整服务端数据未读数不延续客户端本地数据常见误解澄清误解事实联系人列表接口会返回未读消息数不会。ContactVo没有未读字段未读数由客户端环信 SDK 本地维护friend 表是一条记录存一对好友是userIdfriendId双列、双向两条记录查询只按 userId 等值匹配联系人按最近聊天时间排序按created好友建立时间倒序接口没有最近联系时间字段添加好友只写数据库双写先调环信 addContact 注册 IM 关系成功后再写 MongoDB通讯录数据在环信那边列表数据来自本地friend集合 用户资料服务环信只负责消息通道和好友注册环信删好友后本地列表会同步消失不会两边存储独立本地列表照样显示只是聊天发不出去官方文档环信服务端 REST API好友/联系人接口service.contact().add/remove环信客户端 SDK会话与未读数总结联系人管理的需求面很薄但背后有三个值得记住的设计决策。其一社交好友与 IM 联系人共用一份关系添加时对环信和 MongoDB 双写写入顺序「先环信后 Mongo」把不一致风险压到最小。其二好友关系存双向两条记录读路径退化为单字段等值查询读多写少的数据就该这么设计。其三未读数完全交给客户端 SDK 本地维护服务端接口不碰它明确能力边界比堆功能重要如果哪天产品要求多端未读同步再评估服务端统计方案也不迟。下一篇联系人列表代码实现把这些设计落到代码ContactVo、MessagesController的分页接口、FriendApiImpl的save与findByUserId逐段拆解。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询