基于 ZEGO ZIM 搭建 App 内电商客服聊天场景:商品卡片、客服路由与合规审计的实现

2026/08/27

电商客服聊天 ≠ 通用 IM。难点不在"发消息",而在三件事:会话如何路由、商品订单如何结构化表达、客服系统如何与业务系统联动。本文基于 ZEGO ZIM(ZEGO 即时通讯 IM)以 Android 为例,从架构到落代码讲清楚一套可上线的电商客服方案。

一、客服聊天到底难在哪

在实现聊天功能之前,先沿着用户的一次真实咨询走一遍:

用户在商品详情页点了"联系客服" → 发了一张尺码表截图 → 客服秒回一条商品卡片 → 用户问了句"发什么快递"就下单了 → 两天后用户没取件,客服自动跟进了一条物流提醒。

把这条链路拆开,电商客服有四件事是通用聊天工具解决不了的:

  1. 会话入口是业务上下文:用户是带着"哪个商品、哪个订单"来的,聊天窗口必须能携带这些上下文,而不是直接开聊;
  2. 消息不只是文字:尺码表是图片,商品是卡片,物流是模板消息——富媒体和结构化消息是刚需;
  3. 客服是被分配的,不是被找的:几百个客服同时在线,用户发来咨询,谁来接、怎么接,由路由策略决定;
  4. 聊天记录是要审计的:交易纠纷、客诉仲裁都依赖聊天留存,且必须过内容安全审核。

自研 IM?长连接保活、多端同步、离线推送、消息可靠性,每一项都是持续的工程债。接在线客服 SaaS?数据不在自己手里,也无法深度嵌入商品流程。这中间的平衡点,是选择一个成熟的 IM SDK 做消息底座,把路由、卡片、审计这些业务逻辑留在自己的服务端,ZIM 就是典型的这种底座。

二、整体架构:三个角色,一条链路

先建立一个共识:客服也是一个 ZIM 用户

这样整个系统不需要引入任何新的消息模型:用户和客服之间就是一条单聊会话(Peer 会话),会话 ID 天然可以绑定工单号,所有 ZIM 的会话能力(未读数、已读回执、历史消息、置顶)全部直接复用。

┌─────────────┐     单聊(Peer)      ┌─────────────┐
│  用户 App    │ ◄─────────────────► │ 客服工作台   │
│  (ZIM 用户)  │   sendMessage/回调  │ (ZIM 用户)   │
└──────┬──────┘                     └──────┬──────┘
       │ 业务 API(登录态/路由请求)           │
       ▼                                    ▼
┌────────────────────────────────────────────────────┐
│              业务服务端(你的系统)                    │
│  账号体系 │ Token 签发 │ 客服路由 │ 工单 │ 审计归档     │
└────────────────────────────────────────────────────┘

两个关键设计决策:

  • 路由放在服务端,不在客户端。用户点"联系客服"时,客户端只做一件事:调你的业务 API 拿"分配给哪个客服"。路由策略(按商品品类、按客服负载、按用户 VIP 等级)随时调整,客户端零改动;
  • 用户与客服直接 1v1 单聊,不引入客服群组。群聊模型会带来成员管理、权限、@ 规则等一堆和客服场景无关的复杂度。单聊模型下,用户侧始终看到同一个"客服"入口,客服侧转接时只需要在服务端把工单重新绑定到另一个客服的会话,对用户完全透明。

三、前置准备:开通服务与鉴权

ZEGO 控制台创建项目并自助开通 ZIM 服务(ZIM 服务权限不是默认开启的),获取 AppID 和 AppSign。

ZIM 2.3.0 及以上版本同时支持两种鉴权方式:

鉴权方式说明适用场景
AppSign 鉴权客户端内置 AppSign,登录时 Token 传空串开发调试、App 安全性要求不高的场景
Token 鉴权Token 由服务端生成,客户端登录时携带正式上线推荐。AppSign 一旦泄露可被逆向拿到,Token 由服务端控制有效期

建议直接上 Token 鉴权:客服系统的服务端是必须存在的,Token 签发不过是几十行代码。

四、用户端:把聊天做进商品流程

4.1 集成、初始化与登录

在项目级 build.gradle 配置 ZEGO 的 Maven 仓库,应用级添加依赖(版本号以官方发布日志为准):

allprojects {
    repositories {
        maven { url 'https://maven.zego.im' }
        mavenCentral()
        google()
    }
}

dependencies {
    implementation 'im.zego:zim:x.y.z'   // 具体版本见官方发布日志
}

初始化并登录(登录后 ZIM 自动处理长连接、消息同步、离线消息补拉):

// 1. 创建实例(静态方法 create,Android 必须传入 Application)
ZIMAppConfig appConfig = new ZIMAppConfig();
appConfig.appID = 1234567890L;                 // 控制台获取(long 类型)
appConfig.appSign = "your_app_sign";           // 控制台获取

ZIM zim = ZIM.create(appConfig, getApplication());
// 同一进程内可通过 ZIM.getInstance() 复用该实例

// 2. 注册事件回调——消息、连接状态都从这里进来
zim.setEventHandler(new ZIMEventHandler() {
    @Override
    public void onMessageReceived(ZIM zim, ZIMMessageReceivedEventResult result) {
        // 收到新消息:刷新聊天列表 / 更新会话未读数
    }

    @Override
    public void onConnectionStateChanged(ZIM zim, ZIMConnectionState state,
                                         ZIMConnectionEvent event, JSONObject extendedData) {
        // 连接状态变化:可提示用户网络异常
    }
});

// 3. 登录(userID 最大 32 字节,建议与业务账号系统关联,如 "user_10086")
String userID = "user_10086";
ZIMLoginConfig config = new ZIMLoginConfig();
config.userName = "小米";
config.token = token;            // Token 鉴权:服务端签发的 Token
                                 // AppSign 鉴权:传空字符串
zim.login(userID, config, new ZIMLoggedInCallback() {
    @Override
    public void onLoggedIn(ZIMError error) {
        if (error.code == ZIMErrorCode.SUCCESS) {
            // 登录成功,可以开始拉会话列表了
        }
    }
});

两个登录细节值得注意:

  • userID 别用手机号、身份证这类敏感信息,与业务系统关联即可;
  • 支持离线登录(ZIMLoginConfig.isOffline = true),App 启动时可以先用本地缓存的 userID 恢复登录态,聊天记录秒开,再在后台切回在线状态。

4.2 会话列表与未读数:消息中心的正确姿势

客服消息的入口通常是一个"消息中心"列表。ZIM 的会话列表天然支持这些需求:

// 拉取会话列表
ZIMConversationQueryConfig queryConfig = new ZIMConversationQueryConfig();
queryConfig.count = 50;                 // 分页大小
queryConfig.nextConversation = null;    // 分页锚点,首次传 null
zim.queryConversationList(queryConfig, new ZIMConversationListQueriedCallback() {
    @Override
    public void onConversationListQueried(ArrayList conversationList,
                                          ZIMError errorInfo) {
        // conversationList:会话列表(含未读数、最后一条消息、置顶状态)
        // 分页:把列表最后一条会话赋给 nextConversation,继续拉更早的会话
    }
});
// 如需按标记/会话类型过滤,使用带 ZIMConversationFilterOption 的重载

// 查询总未读数,用于 Tab 角标
zim.queryConversationTotalUnreadMessageCount(new ZIMConversationTotalUnreadMessageCountQueriedCallback() {
    @Override
    public void onConversationTotalUnreadMessageCountQueried(int unreadMessageCount, ZIMError errorInfo) {
        // 展示在"消息"Tab 的角标上
    }
});

// 用户点进会话后,清除该会话未读数
zim.clearConversationUnreadMessageCount(conversationID, ZIMConversationType.PEER, callback);

电商场景有个细节:服务通知(物流、优惠券)也建议用单聊会话承载,但要区分"用户主动咨询的客服会话"和"系统通知会话"。做法是在自定义消息里带 msgType 字段,消息中心按类型分组展示,避免把通知和客服消息混在一起影响已读语义。

4.3 消息收发:先选对消息类型

ZIM 的消息类型很全,客服场景里常用的这几类要记牢:

消息类型大小限制客服场景用途
ZIMTextMessage2KB(可申请至 32KB)常规文字对话
ZIMImageMessage10MB(JPG/PNG/GIF 等)用户发截图、尺码表
ZIMAudioMessage6MB / 300 秒(MP3/M4A)语音回复,客服效率利器
ZIMFileMessage100MB售后单、发票文件
ZIMCustomMessage2KB(可申请至 32KB)商品卡片、订单卡片、评价消息
ZIMCombineMessage无限制合并转发多条消息

发消息统一走 sendMessage,重点看发送配置和回调:

// 发送文本消息
ZIMTextMessage textMessage = new ZIMTextMessage("亲,这款支持 7 天无理由退货哦~");

ZIMMessageSendConfig config = new ZIMMessageSendConfig();
config.priority = ZIMMessagePriority.LOW;       // 消息优先级
config.hasReceipt = true;                       // 开启已读回执(见 4.5)
config.pushConfig = buildPushConfig();          // 离线推送配置(见 4.7)

zim.sendMessage(textMessage, serviceUserID, ZIMConversationType.PEER, config,
        new ZIMMessageSentFullCallback() {
    @Override
    public void onMessageAttached(ZIMMessage zimMessage) {
        // 消息已进入发送管道:立即把消息插进 UI,做"发送中"状态
        // 这样弱网时用户也立刻看到气泡,体验更跟手
    }

    @Override
    public void onMessageSent(ZIMMessage zimMessage, ZIMError error) {
        // 发送结果:成功则更新状态,失败按 error.code 处理
    }
});

这里有个体验细节:onMessageAttached 回调是给你做 UI 的——消息一旦通过本地参数校验就先上屏,而不是等网络往返。电商客服里用户发完消息最怕"没反应",这个回调直接决定体感。

4.4 商品卡片:自定义消息的协议设计

商品卡片是电商客服和通用 IM 的分水岭。做法是用 ZIM 自定义消息,把业务结构化数据放进 message 字段,subType 区分消息类别:

// 发送商品卡片(客服端)
String cardJson = new JSONObject()
        .put("cardType", "PRODUCT")                          // 卡片类型
        .put("productId", "P20240815001")
        .put("title", "轻量羽绒服 男女同款")
        .put("price", 399)
        .put("imageUrl", "https://cdn.example.com/p1.jpg")
        .put("action", "buy")                                // 点击行为:跳商品详情
        .toString();

ZIMCustomMessage customMessage = new ZIMCustomMessage(cardJson, 1); // subType=1: 商品卡片
customMessage.searchedContent = "羽绒服";                          // 消息搜索关键词

zim.sendMessage(customMessage, userID, ZIMConversationType.PEER, config, callback);

自定义消息的协议必须两端先行约定,因为 ZIM SDK 不负责解析自定义消息的内容,收到后完全靠你解析。建议在一开始就把 subType 枚举定死:

subType 1: 商品卡片(productId / title / price / imageUrl / action)
subType 2: 订单卡片(orderId / status / amount / trackingUrl)
subType 3: 评价邀请(orderId / rating)
subType 4: 系统通知(type / content / action)

两点提醒:

  • 自定义消息默认 2KB,商品卡片 JSON 里别塞大字段(详情、长文案都别放,卡片只放展示数据和跳转参数);
  • 自定义消息 2.8.0+ 支持发送,接收端低于该版本会显示为未知消息类型,用户端的 SDK 版本要设定最低门槛

4.5 已读回执:把"已读"变成业务信号

客服场景里,"客服已读"和"客服已回复"是两个不同的安抚信号,用户看到"已读"时焦虑就消了一半。ZIM 的已读回执正好可以拆出这两层语义。

发送方在 ZIMMessageSendConfig.hasReceipt = true 标记消息带回执;接收方在 onMessageReceived 里判断消息的 receiptStatus,为 PROCESSING 时调用已读接口:

// 接收方:收到消息后标记已读
List messages = new ArrayList<>();
messages.add(message);

zim.sendMessageReceiptsRead(messages, conversationID, ZIMConversationType.PEER,
        new ZIMMessageReceiptsReadSetCallback() {
    @Override
    public void onMessageReceiptsReadSet(String conversationID, ZIMConversationType conversationType,
                                         ArrayList errorMessageIDs, ZIMError errorInfo) {
        // 标记已读成功
    }
});

// 更简单的做法:整个会话已读(把该会话对方发来的所有消息都标记已读)
zim.sendConversationMessageReceiptRead(conversationID, ZIMConversationType.PEER, callback);

发送方监听 onMessageReceiptChanged 回调,把气泡从"已送达"改成"客服已读":

@Override
public void onMessageReceiptChanged(ZIM zim, ArrayList infos) {
    for (ZIMMessageReceiptChangedInfo info : infos) {
        // info.messageIDs + info.conversationID:定位到对应气泡,更新"已读"状态
    }
}

注意两个工程细节:sendMessageReceiptsRead 一次最多传 10 条消息(超出报错误码 6000282),批量已读时按 10 条切片;需要为历史消息补已读时,先查历史消息、判断回执状态再设置。

4.6 历史消息:换设备、杀进程都不怕

ZIM 的 queryHistoryMessage 查询历史消息时优先从本地数据库缓存检索,本地不完整再自动向服务端拉取。这意味着:

  • 用户杀掉 App 再进来,聊天记录秒开(本地缓存不受服务端存储时长限制);
  • 换设备、重新登录,服务端历史消息自动补全(存储天数与套餐版本相关,正式上线前确认所选套餐);
  • 分页加载通过 nextMessage 消息锚点,支持上滑加载更早的消息。
ZIMMessageQueryConfig queryConfig = new ZIMMessageQueryConfig();
queryConfig.nextMessage = null;              // 分页锚点,首次查询传 null
queryConfig.messageCount = 30;               // 每页条数
queryConfig.reverse = true;                  // 从后往前取(新→旧)

zim.queryHistoryMessage(conversationID, ZIMConversationType.PEER, queryConfig,
        new ZIMMessageQueriedCallback() {
    @Override
    public void onMessageQueried(ArrayList messageList, ZIMError errorInfo) {
        // 上滑加载更多时,把当前列表最后一条消息作为锚点继续拉取
        queryConfig.nextMessage = messageList.get(messageList.size() - 1);
    }
});

4.7 离线推送:客服消息的最后一公里

用户离开聊天页,客服回复了——这时候 App 进程可能都死了。ZIM 的离线推送(ZPNs)覆盖 FCM 和国内厂商通道(小米、华为、OPPO、vivo 等),在控制台配置好推送证书后,发消息时携带 ZIMPushConfig 即可:

ZIMPushConfig pushConfig = new ZIMPushConfig();
pushConfig.title = "客服小云";                              // 通知标题
pushConfig.content = "亲,您咨询的尺码问题回复啦";            // 通知内容
pushConfig.payload = "{\"conversationID\":\"user_10086\"}"; // 自定义数据:点击跳转用
pushConfig.threadID = "customer_service";                  // iOS 通知分组(可选)

客户端配合"自定义点击跳转":用户点击推送后,通过 onNotificationClicked 回调拿到 payload,直接跳进对应会话页,而不是回到首页再找,这个细节直接决定推送转化率。

五、客服端:工作台不是聊天室

客服同时接待几十个会话,工作台的核心诉求是排序和标记,这恰好是 ZIM 会话能力的主场:

// 会话标记:把"待处理/已处理/加急"做成分组
// markType 为整型标记值,含义由业务自定义,如 1=待处理、2=已处理
Integer markType = 1;
boolean enable = true;
ArrayList convList = new ArrayList<>();
ZIMConversationBaseInfo conversation = new ZIMConversationBaseInfo();  // 无参构造
conversation.conversationID = conversationID;
conversation.conversationType = ZIMConversationType.PEER;
convList.add(conversation);

zim.setConversationMark(markType, enable, convList, new ZIMConversationMarkSetCallback() {
    @Override
    public void onConversationMarkSet(ArrayList failedConversationInfos,
                                      ZIMError errorInfo) {
        // 部分会话操作失败时,errorInfo 仍为 SUCCESS,失败列表在 failedConversationInfos 中
    }
});

// 查询时按标记过滤(只拉"待处理"会话):调用带 ZIMConversationFilterOption 的
// queryConversationList(config, option, callback) 重载

客服端值得用的会话能力清单:

能力接口客服工作台用途
会话标记setConversationMark(2.17.0+,单会话标记上限:2.27.0+ 为 60 个)“待处理/已处理"分组、加急
会话置顶服务端置顶会话 API重要用户置顶
会话草稿setConversationDraft(2.14.0+)客服输入一半的回复,切走再回来不丢
引用回复replyMessage引用用户原话回复,纠纷场景必备
消息撤回revokeMessage客服发错商品链接,撤回重发
单聊免打扰setConversationNotificationStatus下班时段静音指定用户

六、服务端:客服系统"活"起来的另一半

6.1 服务端主动发消息

不是所有消息都来自人工客服。物流更新、优惠券发放、客服超时未回复的自动提醒,都由服务端通过 ZIM 服务端 API 直接发送单聊消息,客户端无需感知发送方是人是机。注意服务端发送的消息同样支持离线推送,且国内 Android 厂商通道支持无限频推送。

6.2 回调:业务系统与聊天系统的数据桥

在控制台配置回调地址后,ZIM 服务端会把关键事件 POST 给你的业务后台。客服场景必须接的两个:

  • 消息发送前回调before_send_msg):消息发出前先问业务后台,可以用来记录聊天、拦截违规发言、实现黑白名单;
  • 消息发送后回调​(send_msg):消息发出后实时同步到业务服务器,这是聊天记录归档审计的主路径。客诉仲裁时,全量聊天记录从你库里出,而不是依赖 IM 服务端的存储套餐。

另外还有登录登出回调,可用于统计在线客服人数、空闲客服排班。

官方提醒:回调服务不能保证完全可靠。所以审计归档可以依赖回调 + 定期全量对账兜底,但"路由分配"这类核心流程不要设计成"只靠回调驱动"。

6.3 内容审核与合规

电商客服是高危场景,广告、导流、违禁词随时可能出现。ZIM 的内容审核服务直接覆盖文本、图片、语音、视频消息:

  • 先审后发 / 先发后审两种策略可选:客服场景建议文本消息先审后发(拦截导流),图片先发后审(不阻塞用户体验);
  • 支持自定义敏感词(竞品名、微信号、手机号等导流黑话);
  • 审核结果可配合消息发送前回调做二次决策。

加上 6.2 的全量消息归档,客服系统的合规三件套(内容审核、全程留存、数据可查)就齐了。

七、注意事项清单

  1. 限频:可靠消息单客户端 10 次/秒。恶意刷屏不要指望 SDK 挡,服务端在回调里做频控和黑名单;
  2. 大小边界:文本/自定义消息默认 2KB,商品卡片别塞详情;图片 10MB、语音 300 秒/6MB,发送前客户端先压缩和预检;
  3. 自定义消息协议先行:ZIM 不解析自定义内容,两端 subType 枚举和 JSON schema 必须在联调前定死并做版本兼容;
  4. SDK 版本门槛:自定义消息、会话标记、草稿等能力有版本要求(分别是 2.8.0+、2.17.0+、2.14.0+),老版本用户会静默丢失能力,升级策略要提前定;
  5. 已读回执批量限制sendMessageReceiptsRead 一次 ≤10 条;
  6. 回调不可靠:审计对账要兜底,核心流程别依赖回调;
  7. 历史消息时长按套餐计:正式上线前确认所选套餐的存储天数,长期留存以服务端归档为准。

八、总结:一张表看完整个方案

业务诉求实现方式关键能力
用户与客服对话单聊会话(客服即 ZIM 用户)sendMessage / onMessageReceived
商品/订单卡片自定义消息(subType 协议)ZIMCustomMessage
消息中心与角标会话列表 + 未读数queryConversationList / 未读总数
“客服已读"安抚已读回执hasReceipt / sendMessageReceiptsRead
客服工作台分组会话标记 + 置顶setConversationMark
离线触达离线推送 + 自定义跳转ZIMPushConfig / onNotificationClicked
客服分配与转接服务端路由(业务自实现)服务端 API + 回调
机器人/超时提醒服务端发消息发送单聊消息 API
聊天审计归档发送后回调同步业务库send_msg 回调
内容安全内容审核(先审后发/先发后审)控制台开通,自定义敏感词

后续扩展方向:ZIM 呼叫邀请可直接把"打字聊不明白"的会话升级成语音通话;再往后可以接入 ZEGO 的实时互动 AI Agent,让机器人先接一轮,答不了再转人工——路由策略不变,加一层前置即可。

核心判断:客服系统的技术选型,本质是"消息底座"和"业务逻辑"的分工。把消息可靠送达、多端同步、离线推送交给 ZIM 这种成熟底座,把路由、卡片、审计握在自己手里,是投入产出比最高的做法。

最新文章
数字人直播 Web 端接入实战,1 个 Agent 当天跑通
2026/09/01
基于 ZEGO ZIM 搭建 App 内电商客服聊天场景:商品卡片、客服路由与合规审计的实现
2026/08/27
WebCodecs API 详解:浏览器中的原生编解码器访问
2026/08/25
活动报名|来全球AI出海大会与即构共话增长
2026/08/21
远程医疗视频会议功能实现指南:基于 ZEGO 音视频 SDK 的集成全流程
2026/08/20
扫一扫,获取更多服务与支持
关注我们
获得更多服务与支持了解价格与优惠 扫码关注我们
关注我们
获得更多服务与支持了解价格与优惠 扫码关注我们