七鱼开发文档中心
总览服务端 APIWebiOSAndroid鸿蒙Uniapp微信小程序插件三方接入
在线系统

实时消息推送对接

在线系统实时消息推送的 URL 验证、解密校验、事件数据格式和字段说明。

验证消息的确来自七鱼

当点击“保存”提交以上信息时, 七鱼会发送一条验证消息到填写的URL,发送方法为POST。 企业的接收消息服务器接收到验证请求后,需要作出正确的响应才能通过URL验证

⚠️注:该操作只在保存时进行,保存成功后,进行事件推送没有nonce、echostr字段。

企业在获取请求时需要做Urldecode处理,否则可能会验证不成功

假设接收消息地址设置为:http://api.custom.notify.com,七鱼将向该地址发送如下验证请求:

参数字段类型是否必须参数说明
appkeyString是可在扩展与集成-开发者ID-AppKey中查看
timeInteger是时间戳,单位为秒
nonceString是随机数
echostrString是加密的字符串。需要解密得到消息内容明文
checksumString是加密签名,checksum结合了企业AppSecret、请求中的timestamp、nonce参数、加密的消息体

企业后台收到请求后,需要做如下操作:

  • 对收到的请求做Urldecode处理
  • 解密echostr参数得到消息内容(即msg字段)
  • 在1秒内响应POST请求,响应内容为上一步得到的明文消息内容(不能加引号,不能带bom头,不能带换行符,不能带code,直接返回结果即可)

以上2~3步骤可以直接使用验证URL函数一步到位。 之后接入验证生效,接收消息开启成功。

验证 URL 函数

本函数实现:

  • 签名校验
  • 解密数据包,得到明文消息内容

public String getUrl(@RequestParam("nonce") String nonce,
                        @RequestParam("time") String timestamp,
                        @RequestParam("echostr") String echostr,
                        @RequestParam("checksum") String checksum,
                        @RequestParam("appkey") String appkey) throws Exception {
        String echostrDecode = URLDecoder.decode(echostr, "UTF-8");
        String signature = SHA1Util.getSHA1(appkey, timestamp, nonce, echostrDecode);

        if (!signature.equals(checksum)) {
            return "error";
        }
        String appSecret = ""; //可在扩展与集成-开发者ID-AppSecret中查看
        return decrypt(appSecret, echostrDecode);
}

public static String decrypt(String customMsgSecret, String text) throws Exception {
        byte[] original;
        try {
        // 使用BASE64对密文进行解码
        byte[] encrypted = Base64.getUrlDecoder().decode(text);

        // 解密
        original = cipherData(customMsgSecret, false, encrypted);
        } catch (Exception e) {
            return null;
        }
        String content;
        try {
        // 去除补位字符
        byte[] bytes = PKCS7Encoder.decode(original);
        // 分离16位随机字符串,网络字节序和AppId
        byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
        int xmlLength = recoverNetworkBytesOrder(networkOrder);
        content = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
        } catch (Exception e) {
            return null;
        }
        return content;
}

int recoverNetworkBytesOrder(byte[] orderBytes) {
    int sourceNumber = 0;
    for (int i = 0; i < 4; i++) {
        sourceNumber <<= 8;
        sourceNumber |= orderBytes[i] & 0xff;
    }
    return sourceNumber;
}

public static byte[] cipherData(String customMsgSecret, boolean encrypt, byte[] data) throws Exception {
    byte[] iv = new byte[16];
    System.arraycopy(customMsgSecret.getBytes(), 0, iv, 0, 16);

    CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(customMsgSecret.getBytes()), iv);

    BufferedBlockCipher aes = new BufferedBlockCipher(new CBCBlockCipher(
    new AESEngine()));
    aes.init(encrypt, ivAndKey);

    int minSize = aes.getOutputSize(data.length);
    byte[] outBuf = new byte[minSize];
    int length1 = aes.processBytes(data, 0, data.length, outBuf, 0);
    int length2 = aes.doFinal(outBuf, length1);
    int actualLength = length1 + length2;
    byte[] result = new byte[actualLength];
    System.arraycopy(outBuf, 0, result, 0, result.length);
    return result;
}

class PKCS7Encoder {
    static Charset CHARSET = Charset.forName("utf-8");
    static int BLOCK_SIZE = 32;

    /**
     * 获得对明文进行补位填充的字节.
     *
     * @param count 需要进行填充补位操作的明文字节个数
     * @return 补齐用的字节数组
     */
    static byte[] encode(int count) {
        // 计算需要填充的位数
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        if (amountToPad == 0) {
            amountToPad = BLOCK_SIZE;
        }
        // 获得补位所用的字符
        char padChr = chr(amountToPad);
        String tmp = new String();
        for (int index = 0; index < amountToPad; index++) {
            tmp += padChr;
        }
        return tmp.getBytes(CHARSET);
    }

    /**
     * 删除解密后明文的补位字符
     *
     * @param decrypted 解密后的明文
     * @return 删除补位字符后的明文
     */
    static byte[] decode(byte[] decrypted) {
        int pad = (int) decrypted[decrypted.length - 1];
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }

    /**
     * 将数字转化成ASCII码对应的字符,用于对明文进行补码
     *
     * @param a 需要转化的数字
     * @return 转化得到的字符
     */
    static char chr(int a) {
        byte target = (byte) (a & 0xFF);
        return (char) target;
    }

}

public class SHA1Util {

    /**
     * 用SHA1算法生成安全签名
     * @param token 票据
     * @param timestamp 时间戳
     * @param nonce 随机字符串
     * @param encrypt 密文
     * @return 安全签名
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt)
    {
        try {
            String[] array = new String[] { token, timestamp, nonce, encrypt };
            StringBuffer sb = new StringBuffer();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

推送数据格式

该服务能够提供三个事件推送:

事件类型说明
CLIENT_MARK_READ会话已读事件
STAFF_MESSAGE坐席消息
CLIENT_MESSAGE访客消息

请求方式:POST 请求地址:http://api.custom.notify.com/?appkey=APP_KEY&eventType=STAFF_MESSAGE&time=162593272&checksum=5dfb898d9fw99dg99dfs3sddsaf9&checksumAlgorithm=0​

CLIENT_MESSAGE 数据格式

{
"createtime": 1625932727657,
"msgidclient": "6383959733#0#36946076bba94a2bb0780bf748606f8c",
"session_id": 6383959733,
"content": "投诉",
"from_user": 1,
"foreign_id": "81275614-ff4c-490e-9517-0934f4fc9325@顺风车乘客",
"autoreply": 0,
"user_id": 20627473396,
"staff_id": 4126122,
"id": 31050959281,
"updatetime": 1625932727657,
"corp_id": 3517575,
"msgtype": "TEXT",
"status": 1
}

STAFF_MESSAGE 数据格式

{
"createtime": 1625932729758,
"msgidclient": "6383962892#7e2bc2663af43b5f295d185a2356b0b7",
"session_id": 6383962892,
"content": "请问您还有其他问题需要帮助的吗?",
"from_user": 0,
"foreign_id": "da27aef3-9888-4fb1-9e82-f3fa4764b65b@顺风车乘客",
"autoreply": 0,
"user_id": 17854991208,
"staff_id": 6016458,
"id": 31050960420,
"updatetime": 1625932729758,
"corp_id": 3517575,
"msgtype": "TEXT",
"status": 1,
"useLeave": 1
}

CLIENT_MARK_READ 数据格式

{
"foreign_id": "da27aef3-9888-4fb1-9e82-f3fa4764b65b@顺风车乘客",
"user_id": 17854991208,
"lastest_read_time": 1625932730420,
"staff_id": 6016458,
"session_id": 6383962892,
"corp_id": 3517575
}

参数说明

字段数据库中字段类型和长度描述规则细节
createtimebigint(20)消息创建时间单位毫秒
msgidclientvarchar(128)消息幂等键(客户端消息 ID)可用于消息推送的去重处理
session_idbigint(20)会话ID全局唯一
msgtypevarchar(32)消息类型常见类型:TEXT(文本)、RICHTEXT(富文本)、PICTURE(图片)、AUDIO(音频)、FILE(文件)、VIDEO(视频)、CUSTOM(自定义消息)、CARD_MSG(卡片消息)。完整说明见消息类型说明
contentvarchar(10240)消息内容内容格式由 msgtype 决定,详见消息类型说明
from_usertinyint(2)消息发送方1=来自访客;0=来自客服
foreign_idvarchar(255)访客ID由企业自定义,并通过前端能力给到七鱼
autoreplytinyint(4)是否是自动回复1=自动;0=手动(人工回复)
user_idbigint(20)访客ID七鱼内部访客ID
staff_idbigint(20)客服ID七鱼内部客服ID
updatetimebigint(20)消息更新时间单位毫秒
corp_idbigint(20)企业ID
statustinyint(2)消息状态1-正常; 2-已撤回
lastest_read_timebigint(20)最后阅读时间单位毫秒或为空(未读)
useLeavetinyint(2)访客是否离开0-未离开;1-离开

这篇文档有帮助吗?

目录