在线系统
实时消息推送对接
在线系统实时消息推送的 URL 验证、解密校验、事件数据格式和字段说明。
验证消息的确来自七鱼
当点击“保存”提交以上信息时, 七鱼会发送一条验证消息到填写的URL,发送方法为POST。 企业的接收消息服务器接收到验证请求后,需要作出正确的响应才能通过URL验证
⚠️注:该操作只在保存时进行,保存成功后,进行事件推送没有nonce、echostr字段。
企业在获取请求时需要做Urldecode处理,否则可能会验证不成功假设接收消息地址设置为:http://api.custom.notify.com,七鱼将向该地址发送如下验证请求:
| 参数 | 字段类型 | 是否必须 | 参数说明 |
|---|---|---|---|
| appkey | String | 是 | 可在扩展与集成-开发者ID-AppKey中查看 |
| time | Integer | 是 | 时间戳,单位为秒 |
| nonce | String | 是 | 随机数 |
| echostr | String | 是 | 加密的字符串。需要解密得到消息内容明文 |
| checksum | String | 是 | 加密签名,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
}参数说明
| 字段 | 数据库中字段类型和长度 | 描述 | 规则细节 |
|---|---|---|---|
| createtime | bigint(20) | 消息创建时间 | 单位毫秒 |
| msgidclient | varchar(128) | 消息幂等键(客户端消息 ID) | 可用于消息推送的去重处理 |
| session_id | bigint(20) | 会话ID | 全局唯一 |
| msgtype | varchar(32) | 消息类型 | 常见类型:TEXT(文本)、RICHTEXT(富文本)、PICTURE(图片)、AUDIO(音频)、FILE(文件)、VIDEO(视频)、CUSTOM(自定义消息)、CARD_MSG(卡片消息)。完整说明见消息类型说明 |
| content | varchar(10240) | 消息内容 | 内容格式由 msgtype 决定,详见消息类型说明 |
| from_user | tinyint(2) | 消息发送方 | 1=来自访客;0=来自客服 |
| foreign_id | varchar(255) | 访客ID | 由企业自定义,并通过前端能力给到七鱼 |
| autoreply | tinyint(4) | 是否是自动回复 | 1=自动;0=手动(人工回复) |
| user_id | bigint(20) | 访客ID | 七鱼内部访客ID |
| staff_id | bigint(20) | 客服ID | 七鱼内部客服ID |
| updatetime | bigint(20) | 消息更新时间 | 单位毫秒 |
| corp_id | bigint(20) | 企业ID | |
| status | tinyint(2) | 消息状态 | 1-正常; 2-已撤回 |
| lastest_read_time | bigint(20) | 最后阅读时间 | 单位毫秒或为空(未读) |
| useLeave | tinyint(2) | 访客是否离开 | 0-未离开;1-离开 |
这篇文档有帮助吗?