客服系统源码对接用户数据-99客服

用户数据对接教程

客服系统可用于电商、金融、通讯、公益等多种场景。在这些场景中,客服系统承担着连接用户与平台的重要角色:既帮助平台识别访客、理解诉求,也帮助用户获得一致、可信赖的服务体验,从而提升品牌认知、满意度与粘性。

在实际对接中,常见需求是:业务系统里已经登录的用户,在发起咨询时,客服端要能识别「是谁在咨询」,并展示其用户标识、头像、昵称,必要时还可同步订单等业务信息。为此,系统预留了用户数据对接能力:在网页脚本或访问地址中传入相应参数,即可完成对接。

本文按「先易后难」说明:明文参数对接安全与加密对接、以及 PHP / Java 生成加密参数 的示例代码。


一、对接方式概览

系统支持两种入口形态:

方式 说明
JS 代码接入 在页面中嵌入一段脚本,通过全局变量 window._kefu 传入组织标识、分组及用户信息等,由脚本加载客服界面。
URL 直接接入 用户或业务系统直接打开访客页地址,通过查询参数传递用户信息。

无论哪种方式,常用的用户字段为:

参数 含义 说明
uid 用户 ID 在您业务系统中的唯一标识,必填(明文或解密后均需能识别用户)。
name 昵称 可选,未传时系统可使用默认访客名。
avatar 头像地址 可选,建议为 https 可访问的完整 URL。

此外,脚本中通常还需要配置:

参数 含义
bid 组织标识,由后台提供。
groupid 客服分组,无特殊需求可填 '0'
domain 客服系统访问域名,需与部署一致(如 https://kefu.99kf.com)。
mini 1(默认)为页面内弹层;0 为新开窗口。
themeColor 主题色:auto 表示尝试从当前页面自动推断;也可传 #1677ff 等固定色。

二、明文对接(简单场景)

明文对接指在 URL 或 window._kefu直接传递 uidnameavatar。接入成本低,适合内网、测试或对伪造风险不敏感的场景。

请注意: 若他人在知道您的 bid 的前提下,理论上可以编造 uid 等参数访问。对安全要求较高的业务,请阅读本文第三节「加密对接」

2.1 JS 代码接入(当前推荐写法)

下面是一段与当前脚本兼容的接入示例:在 window._kefu 中配置 bidgroupiddomainminithemeColor,并传入用户 uidnameavatar

<script>
  window._kefu = {
    bid: 'Z3SmMhtO',
    groupid: '0',
    domain: 'https://kefu.99kf.com',
    mini: 1,
    themeColor: 'auto',
    uid: '102',
    name: '大大',
    avatar: 'https://example.com/avatar.jpg'
  };
  (function () {
    var d = document;
    function l() {
      var a = d.createElement('script');
      a.type = 'text/javascript';
      a.async = true;
      a.charset = 'utf-8';
      a.src = 'https://kefu.99kf.com/static/js/kf.js';
      var b = d.getElementsByTagName('script')[0];
      b.parentNode.insertBefore(a, b);
    }
    l();
  })();
</script>

说明:

2.2 URL 直接接入

用户可直接打开如下形式的地址(示例域名请替换为实际客服域名):

https://kefu.99kf.com/user?bid=Z3SmMhtO&groupid=0&uid=100&name=小小&avatar=https%3A%2F%2Fexample.com%2Favatar.jpg

重要: 使用 URL 传递 avatar(或其它含特殊字符的参数)时,应对参数值做 URL 编码,例如:

否则可能导致头像无法解析或整段地址被截断。

2.3 对接后的效果

完成对接后,客服侧会话列表与详情中可看到对应用户的标识、头像、昵称等信息(具体界面以实际版本为准)。 截图


三、加密对接(推荐用于生产环境)

3.1 为什么需要加密

明文方式下,仅凭链接或前端脚本即可仿造 uid,存在被恶意利用的风险。为此,系统提供:

  1. 接口密钥(每组织唯一,长度 32 字符):用于对访客身份数据进行 AES-256-GCM 加密,生成 code 参数。
  2. 是否允许明文传输用户数据(后台配置项):关闭后,不再接受在 URL 中直接携带 uidnameavatar必须使用加密后的 code 访问。

这样,只有掌握接口密钥的一方才能生成有效 code,从而降低伪造身份的风险。

3.2 明文与加密在形式上的区别

明文(允许明文开启时),地址形态类似:

https://kefu.99kf.com/user?bid=您的bid&groupid=0&uid=100&name=昵称&avatar=编码后的头像URL

加密(推荐),不再在查询串中暴露 uid 等明文,而是携带一段 Base64 密文,形态类似:

https://kefu.99kf.com/user?bid=您的bid&groupid=0&code=Base64密文字符串

code 由服务端使用该组织的接口密钥对一段查询字符串(如 uid=...&name=...&avatar=...)加密后得到;访客页首次加载后会重定向到带 code 的规范地址,服务端解密后识别用户。

3.3 密文格式说明(与系统实现一致)

便于与多语言对接,加密规则约定如下:

项目 说明
算法 AES-256-GCM
密钥 接口密钥字符串,必须为 32 个字符(UTF-8 下 32 字节)
IV 12 字节,每次加密随机生成
认证标签 16 字节
二进制排列 IV(12 字节) + Tag(16 字节) + 密文
传输 对上述二进制做 Base64,作为 code 的值

待加密的明文为 UTF-8 编码的查询字符串,与 PHP http_build_query($fields) 默认行为一致,例如:

uid=user001&name=张三&avatar=https%3A%2F%2Fexample.com%2Fa.png

四、生成加密数据:PHP 示例

以下函数生成可放入 URL 的 code(与系统内置逻辑一致)。请将 接口密钥 替换为后台显示的 32 位密钥。

<?php

/**
 * @param string $apiKey 32 字符的接口密钥
 * @param array  $fields 如 ['uid' => 'xxx', 'name' => '昵称', 'avatar' => 'https://...']
 */
function buildVisitorCode(string $apiKey, array $fields): string
{
    if (strlen($apiKey) !== 32) {
        throw new InvalidArgumentException('接口密钥长度必须为 32');
    }

    $plain = http_build_query($fields);

    $cipher = 'aes-256-gcm';
    $ivLen = 12;
    $tagLen = 16;

    $iv = random_bytes($ivLen);
    $tag = '';

    $ciphertext = openssl_encrypt(
        $plain,
        $cipher,
        $apiKey,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
        '',
        $tagLen
    );

    if ($ciphertext === false) {
        throw new RuntimeException('加密失败');
    }

    return base64_encode($iv . $tag . $ciphertext);
}

// 使用示例
$apiKey = '0123456789abcdef0123456789abcdef'; // 从后台复制,示例勿用于生产
$code = buildVisitorCode($apiKey, [
    'uid'    => 'user_' . time(),
    'name'   => '访客昵称',
    'avatar' => 'https://example.com/avatar.png',
]);

$bid = '您的bid';
$url = 'https://kefu.99kf.com/user?bid=' . rawurlencode($bid)
    . '&groupid=0&code=' . rawurlencode($code);

拼接完整 URL 时,请对 code 使用 rawurlencode(),避免 +/ 等字符在传输中被错误解析。


五、生成加密数据:Java 示例(与 PHP 兼容)

Java 中 Cipher 在 AES-GCM 下,doFinal 的结果一般为 「密文 + Tag(16 字节在后)」,而 PHP 侧存储顺序为 「IV + Tag + 密文」。因此需要拆分 doFinal 结果,再按 PHP 相同顺序组装后 Base64。

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

public class VisitorCodeCrypto {

    private static final int IV_LEN = 12;
    private static final int TAG_BITS = 128;
    private static final String TRANS = "AES/GCM/NoPadding";

    public static String buildVisitorCode(String apiKey, Map<String, String> fields) throws Exception {
        if (apiKey == null || apiKey.length() != 32) {
            throw new IllegalArgumentException("接口密钥长度必须为 32");
        }

        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (Map.Entry<String, String> e : fields.entrySet()) {
            if (!first) sb.append('&');
            first = false;
            sb.append(java.net.URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8));
            sb.append('=');
            sb.append(java.net.URLEncoder.encode(String.valueOf(e.getValue()), StandardCharsets.UTF_8));
        }
        String plain = sb.toString();

        byte[] keyBytes = apiKey.getBytes(StandardCharsets.UTF_8);
        if (keyBytes.length != 32) {
            throw new IllegalArgumentException("接口密钥 UTF-8 字节长度必须为 32");
        }

        byte[] iv = new byte[IV_LEN];
        new SecureRandom().nextBytes(iv);

        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        GCMParameterSpec spec = new GCMParameterSpec(TAG_BITS, iv);
        Cipher cipher = Cipher.getInstance(TRANS);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);

        byte[] cipherWithTag = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
        int tagLen = 16;
        int ctLen = cipherWithTag.length - tagLen;
        byte[] ciphertext = java.util.Arrays.copyOfRange(cipherWithTag, 0, ctLen);
        byte[] tag = java.util.Arrays.copyOfRange(cipherWithTag, ctLen, cipherWithTag.length);

        byte[] packed = new byte[iv.length + tag.length + ciphertext.length];
        System.arraycopy(iv, 0, packed, 0, iv.length);
        System.arraycopy(tag, 0, packed, iv.length, tag.length);
        System.arraycopy(ciphertext, 0, packed, iv.length + tag.length, ciphertext.length);

        return Base64.getEncoder().encodeToString(packed);
    }
}

兼容性要点:


六、加密方式下的 JS 与 URL 示例

6.1 URL 方式

由服务端生成 code 后,拼接:

https://kefu.99kf.com/user?bid=您的bid&groupid=0&code=加密得到的code

6.2 JS 方式

在服务端渲染页面时,将生成的 code 写入 window._kefu.code(与 uid/name/avatar 不要同时使用,以 code 为准):

<script>
  window._kefu = {
    bid: 'Z3SmMhtO',
    groupid: '0',
    domain: 'https://kefu.99kf.com',
    mini: 1,
    themeColor: 'auto',
    code: '此处填入服务端生成的code'
  };
  (function () {
    var d = document;
    function l() {
      var a = d.createElement('script');
      a.type = 'text/javascript';
      a.async = true;
      a.charset = 'utf-8';
      a.src = 'https://kefu.99kf.com/static/js/kf.js';
      var b = d.getElementsByTagName('script')[0];
      b.parentNode.insertBefore(a, b);
    }
    l();
  })();
</script>

七、常见问题

问:接口密钥泄露怎么办?
答:在后台重新生成密钥,并更新所有服务端生成 code 的逻辑。旧密钥产生的 code 将失效。

问:明文和加密可以同时用吗?
答:若配置了 code,接入脚本会优先走加密参数,不再附带明文 uid/name/avatar 查询串。

问:关闭「允许明文」后,旧链接还能用吗?
答:带明文用户参数的链接将被拒绝,请改为使用 code 方式。