Java 项目实战: 交友项目 - 环信后台发送消息测试

概述

用户体系集成代码写完后,最直接的验证方式不是马上写测试代码,而是登录环信 Console 手工给项目用户发一条消息,App 端收到了,「注册 → 连接 → 投递」整条链路就是通的。

纲要

  • 验证思路:为什么用 Console 手工测试
  • 操作步骤:Console 定位用户 → 发送 REST 消息 → App 端确认收到
  • 消息格式:系统消息的 JSON 体约定(与 replyQuestions 的组装逻辑呼应)
  • 验证要点清单(含离线消息)
  • REST API 等价验证(curl 示例,适合自动化回归)
  • 排错表 + 常见误解澄清

验证思路

前面两步已完成:注册时通过 HuanXinTemplate.createUser() 把用户同步注册到环信,客户端通过 GET /huanxin/user 拿到环信账号密码后自动登录(日志里能看到「已连接」)。这两点都成立时,集成代码本身就没问题了,剩下的疑问只有一个:消息到底能不能送达到这台设备?

这时候用环信管理后台手工发一条消息是最快的验证手段:
#mermaid-svg-DTy2S17zLP4M0x8l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DTy2S17zLP4M0x8l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DTy2S17zLP4M0x8l .error-icon{fill:#552222;}#mermaid-svg-DTy2S17zLP4M0x8l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DTy2S17zLP4M0x8l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DTy2S17zLP4M0x8l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DTy2S17zLP4M0x8l .marker.cross{stroke:#333333;}#mermaid-svg-DTy2S17zLP4M0x8l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DTy2S17zLP4M0x8l p{margin:0;}#mermaid-svg-DTy2S17zLP4M0x8l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DTy2S17zLP4M0x8l .cluster-label text{fill:#333;}#mermaid-svg-DTy2S17zLP4M0x8l .cluster-label span{color:#333;}#mermaid-svg-DTy2S17zLP4M0x8l .cluster-label span p{background-color:transparent;}#mermaid-svg-DTy2S17zLP4M0x8l .label text,#mermaid-svg-DTy2S17zLP4M0x8l span{fill:#333;color:#333;}#mermaid-svg-DTy2S17zLP4M0x8l .node rect,#mermaid-svg-DTy2S17zLP4M0x8l .node circle,#mermaid-svg-DTy2S17zLP4M0x8l .node ellipse,#mermaid-svg-DTy2S17zLP4M0x8l .node polygon,#mermaid-svg-DTy2S17zLP4M0x8l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DTy2S17zLP4M0x8l .rough-node .label text,#mermaid-svg-DTy2S17zLP4M0x8l .node .label text,#mermaid-svg-DTy2S17zLP4M0x8l .image-shape .label,#mermaid-svg-DTy2S17zLP4M0x8l .icon-shape .label{text-anchor:middle;}#mermaid-svg-DTy2S17zLP4M0x8l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DTy2S17zLP4M0x8l .rough-node .label,#mermaid-svg-DTy2S17zLP4M0x8l .node .label,#mermaid-svg-DTy2S17zLP4M0x8l .image-shape .label,#mermaid-svg-DTy2S17zLP4M0x8l .icon-shape .label{text-align:center;}#mermaid-svg-DTy2S17zLP4M0x8l .node.clickable{cursor:pointer;}#mermaid-svg-DTy2S17zLP4M0x8l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DTy2S17zLP4M0x8l .arrowheadPath{fill:#333333;}#mermaid-svg-DTy2S17zLP4M0x8l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DTy2S17zLP4M0x8l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DTy2S17zLP4M0x8l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DTy2S17zLP4M0x8l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DTy2S17zLP4M0x8l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DTy2S17zLP4M0x8l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DTy2S17zLP4M0x8l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DTy2S17zLP4M0x8l .cluster text{fill:#333;}#mermaid-svg-DTy2S17zLP4M0x8l .cluster span{color:#333;}#mermaid-svg-DTy2S17zLP4M0x8l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DTy2S17zLP4M0x8l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DTy2S17zLP4M0x8l rect.text{fill:none;stroke-width:0;}#mermaid-svg-DTy2S17zLP4M0x8l .icon-shape,#mermaid-svg-DTy2S17zLP4M0x8l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DTy2S17zLP4M0x8l .icon-shape p,#mermaid-svg-DTy2S17zLP4M0x8l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DTy2S17zLP4M0x8l .icon-shape .label rect,#mermaid-svg-DTy2S17zLP4M0x8l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DTy2S17zLP4M0x8l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DTy2S17zLP4M0x8l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DTy2S17zLP4M0x8l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 在线
离线
Console 手工发消息
环信服务器
客户端在线?
即时投递
存离线消息
上线补拉
App 展示陌生人消息

它的好处在于变量少:Console 代表环信服务器一侧,消息由它直接下发,绕过了交友服务端的业务代码。App 收到,问题范围就缩小到「业务侧怎么构造消息」;App 收不到,问题就在「注册或连接」这两层。比一上来就写端到端测试代码快得多,测试代码失败时,还得先排除是不是自己代码的问题。

Console 操作步骤

前提:手机端已自动登录环信(验证码登录后看到「已连接」状态),目标用户是 hx106(用户 ID 106,按 hx + id 规则注册)。

  1. 登录环信 Console(https://console.easemob.com),选择项目对应的应用(确认 appkey 与工程 HuanXinProperties 配置一致,查错应用是新手高频失误)。
  2. 进入运营管理 → 用户管理 ,在用户列表中找到 hx106。列表里能看到一批 hx 开头的用户,本身就说明「注册到环信」这层已经通过。
  3. 点开该用户右侧的更多 ,选择发送 REST 消息(即模拟服务端通过 REST API 下发消息,发送方为 admin)。
  4. 填写消息内容后点击发送,后台提示发送成功。

消息内容不能随便写。这条消息在客户端会被解析并做个性化展示(陌生人消息列表、昵称、头像跳转),所以必须遵循约定的 JSON 格式:

json 复制代码
{"userId":106,"huanXinId":"hx106","nickname":"小妹","strangerQuestion":"你喜欢去看蔚蓝的大海还是去爬巍峨的高山?","reply":"我喜欢秋天的落叶,夏天的泉水,冬天的雪地,只要有你一切皆可~"}

这个格式与后续 mfService#replyQuestions() 的组装逻辑完全一致:

java 复制代码
//回复陌生人问题
public void replyQuestions(Long userId, String reply) {
    //1、构造消息数据
    Long currentUserId = UserHolder.getUserId();
    UserInfo userInfo = userInfoApi.findById(currentUserId);
    Map map = new HashMap();
    map.put("userId",currentUserId);
    map.put("huanXinId", Constants.HX_USER_PREFIX+currentUserId);
    map.put("nickname",userInfo.getNickname());
    map.put("strangerQuestion",strangerQuestions(userId));
    map.put("reply",reply);
    String message = JSON.toJSONString(map);
    //2、调用template对象,发送消息(参数:接收方的环信id,消息内容)
    Boolean aBoolean = template.sendMsg(Constants.HX_USER_PREFIX + userId, message);
    if(!aBoolean) {
        throw  new BusinessException(ErrorResult.error());
    }
}

发送成功后回到 App 端。客户端不会自动刷新,切换一下页面(退出再进陌生人消息页)即可看到新消息。点进去能看到:昵称、消息内容、头像。

有个细节值得说:头像不是我们发过去的 。JSON 体里只有 userId,客户端解析拿到 ID 后,再回调交友服务端的「环信 ID 查用户详情」接口(MessagesService#findUserInfoByHuanxin(),底层走 UserApi.findByHuanxin() 按 hx_user 字段查库)获取头像和资料。所以 App 上头像能正常显示,同时验证了第三层,服务端查询接口,也是通的。

验证要点

验证项 操作 预期 不通过说明什么
用户已注册 Console 用户管理列表查找 hx 开头用户 能找到 hx106 注册逻辑没跑通,或注册时 createUser 返回 false 后未回写 hxUser 字段
消息投递 Console 发消息,App 在线 几秒内收到,发送方为 admin App 未登录环信、环信 ID 拼写错误、appkey 不一致
消息格式 发标准 JSON 体 App 解析出昵称并展示消息内容 JSON 字段名与客户端约定不符,客户端静默丢弃或显示异常
服务端查询 点击消息查看头像/详情 头像正常加载 findByHuanxin 查不到记录(库里 hx_user 为空)或 MessagesService 接口异常
离线消息 杀掉 App 后发消息,再启动 App 上线后补拉到这条消息 环信侧离线机制正常时不会发生;若丢失需检查客户端初始化与补拉逻辑

REST API 等价验证

Console 的「发送 REST 消息」本质就是调环信 REST API,用 curl 完全等价,且适合写进回归脚本:

bash 复制代码
# 第一步:获取 token(app 的 client_id / client_secret 在 Console 应用详情页)
curl -X POST "https://a1.easemob.com/{org_name}/{app_name}/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YXA6i-Ak8Ol4EeSO0VcBzaqg5A",
    "client_secret": "YXA6VunqiNxoB7IwXHInk1cGiXOOJfc"
  }'

# 第二步:给 hx106 发送文本消息(target_type=users 表示单用户,from=admin)
curl -X POST "https://a1.easemob.com/{org_name}/{app_name}/messages/users" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "target_type": "users",
    "target": ["hx106"],
    "from": "admin",
    "msg": {"type": "txt", "msg": "{\"userId\":106,\"huanXinId\":\"hx106\",\"nickname\":\"小妹\",\"reply\":\"测试回复\"}"}
  }'

注意 msg 里嵌的是 JSON 字符串,内层双引号必须转义,这正是环信文本消息只认字符串、业务语义由内容自携带的体现。工程里的 HuanXinTemplate.sendMsg() 走的是环信 Java SDK(EMService#message().send()),与上面 curl 调的是同一组服务端接口,三种方式(Console / curl / SDK)可互相印证。

排错表

现象 可能原因 排查手法
App 收不到消息 用户未注册到环信 Console 用户列表确认 hx106 存在
App 收不到消息 环信 ID 拼错(如 Hx106 大写) SDK 的 createUser 内部做了 toLowerCase(),手工发时也统一小写
App 收不到消息 App 未登录环信 看客户端「已连接」日志;检查 GET /huanxin/user 返回的账号密码
收到但显示异常 消息不是约定 JSON 格式 对照 replyQuestions 的字段清单逐项核对
Console 找不到用户 注册失败或查错应用 确认 Console 顶部 appkey 与 yml 中 mf.huanxin.appkey 一致

常见误解澄清

误解 事实
Console 发消息就是「后台用户在聊天」 发送方是 admin(管理员账号),模拟的是服务端下发的系统消息,与后续好友聊天的 user-to-user 消息走同一投递通道
消息内容随便写,能收到就行 项目客户端按约定 JSON 解析做个性化展示,格式不对会被解析失败,反而误判链路有问题
头像应该随消息一起发 JSON 体只携带 userId,头像由客户端拿到 ID 后回调服务端接口查询,减小消息体积
集成验证要写一堆测试代码 Console 手工发一条消息即可验证主干链路,测试代码更适合留给后续的自动化回归

总结

这一步是分层验证 的实践:先验证用户注册(Console 用户列表能看到 hx 开头的用户)→ 再验证消息投递(Console 手工发一条,App 能收)→ 最后验证业务消息(后续「陌生人回答送达」场景复用同一 JSON 格式)。逐层排除,问题定位永远只在一层之内。

这个方法论可以推广:任何涉及第三方系统的集成,都优先找一个「绕过自己代码、直达对方系统」的手工操作点,把链路切成两半,先确认对面是通的,再回头看自己这一侧。与发布测试篇「先验证服务起了、再验证接口通了、最后验证业务对了」的验证路径是同一个思路。

相关推荐
用户3721574261351 小时前
Java 操作 Word 脚注和尾注:添加、读取与删除
java
小嘉丶学长2 小时前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节
字节探索2 小时前
Spring Boot 接口幂等性设计:从唯一索引到 Redis+Lua 的工程实践
java·spring boot
用户3721574261352 小时前
Java 实现 Excel 与 Markdown 相互转换:表格导出、格式设置与多表提取
java
SL_staff2 小时前
制造业订单交付总踩坑?用结构化节点协同模型重构计划系统
java·开源·全栈
永遠_3 小时前
YAML格式化工具
java
曼岛_3 小时前
[图解密码学]第4章 古典密码
java·数据库·密码学
一枕槐安4873 小时前
RAG 服务扛住高并发:缓存、队列、限流三件套
java·spring·缓存·异步任务·rag·断路器
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 动态查询开工:昨日回顾与今日任务
java·python·交友