概述
用户体系集成代码写完后,最直接的验证方式不是马上写测试代码,而是登录环信 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 规则注册)。
- 登录环信 Console(https://console.easemob.com),选择项目对应的应用(确认 appkey 与工程
HuanXinProperties配置一致,查错应用是新手高频失误)。 - 进入运营管理 → 用户管理 ,在用户列表中找到
hx106。列表里能看到一批hx开头的用户,本身就说明「注册到环信」这层已经通过。 - 点开该用户右侧的更多 ,选择发送 REST 消息(即模拟服务端通过 REST API 下发消息,发送方为 admin)。
- 填写消息内容后点击发送,后台提示发送成功。
消息内容不能随便写。这条消息在客户端会被解析并做个性化展示(陌生人消息列表、昵称、头像跳转),所以必须遵循约定的 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 格式)。逐层排除,问题定位永远只在一层之内。
这个方法论可以推广:任何涉及第三方系统的集成,都优先找一个「绕过自己代码、直达对方系统」的手工操作点,把链路切成两半,先确认对面是通的,再回头看自己这一侧。与发布测试篇「先验证服务起了、再验证接口通了、最后验证业务对了」的验证路径是同一个思路。