企业微信会话存档JavaSDK使用

微信图片_20210930135025.jpg

最近工作中一个需求是对接企业微信,在同步聊天记录时发现企业微信的官方文档写的比较简单,有些细节在官方文档中并没有详细说明,最后费了好大的力气才搞好,今天这篇文章便来详细的记录下对接的流程,希望能对有此需求的同行提供些许的帮助。我的项目是SpringBoot项目,在文末也会给出相关实例项目的github地址。

下载官网提供的SDK

从官方文档下载SDK,下载下来会看到其提供了windowslinux两个系统的动态库文件,如下:

image.png

在Java中我们可以通过System类加载动态库,有如下两个方法

  • System.loadLibrary 使用该方法需要将动态库文件放到Java可自动加载的目录 可以通过System.getProperty("java.library.path")进行查看
  • System.load 该方法是通过绝对路径加载动态库文件 建议使用该方式 开发环境自己存到本地电脑某个位置即可,生产环境找相关人员放到服务器某个位置

然后在官网提供的Finance类放到我们项目中的com.tencent.wework包下并增加如下代码:

static {
    if (isWindows()) {
        System.load(path.concat("D:\\develop\\lib\\libcrypto-1_1-x64.dll"));
        System.load(path.concat("D:\\develop\\lib\\libssl-1_1-x64.dll"));
        System.load(path.concat("D:\\develop\\lib\\libcurl-x64.dll"));
        System.load(path.concat("D:\\develop\\lib\\WeWorkFinanceSdk.dll"));
    } else {
        System.load("/tmp/libWeWorkFinanceSdk_Java.so");
    }
}

public static boolean isWindows() {
    String osName = System.getProperties().getProperty("os.name");
    return osName.toUpperCase().contains("WIN");
}
复制代码

创建RSAUtil工具类

从企业微信拉下来的聊天记录是使用RSA加密的,需要使用RSA的私钥进行解密,这里在官方文档有说明,我们需要生成一个RSA密钥对,将公钥配置到企业微信,私钥我们自行保存,工具类内容如下:

/**
 * 使用私钥进行RSA解密
 *
 * @param content    需要解密的内容
 * @param privateKey 私钥
 * @return 解密后的内容
 */
public static String decrypt(String content, String privateKey) throws Exception {
    if (StringUtils.isBlank(content)) {
        return "";
    }
    // Base64解码加密后的字符串
    byte[] inputByte = Base64.decodeBase64(content.getBytes("UTF-8"));
    // Base64编码的私钥
    byte[] decoded = Base64.decodeBase64(privateKey);
    PrivateKey priKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
    // RSA解密
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.DECRYPT_MODE, priKey);
    return new String(cipher.doFinal(inputByte));
}
复制代码

使用Finance

在使用之前,需要获取到公私钥,并将公钥、secret和ip配置到企业微信的绘画存档上。

完成上面的步骤,我们就可以使用Finance类进行聊天记录同步了。

相应的错误码如下:

10000 参数错误,请求参数错误
10001 网络错误,网络请求错误
10002 数据解析失败
10003 系统失败
10004 密钥错误导致加密失败
10005 fileid错误
10006 解密失败
10007 找不到消息加密版本的私钥,需要重新传入私钥对
10008 解析encrypt_key出错
10009 ip非法
10010 数据过期
复制代码

创建SDK

long sdk = Finance.NewSdk();
Finance.Init(sdk, corpId, secret);
复制代码

获取chatData列表

/**
 * 获取chatData列表
 *
 * @param seq   查询偏移量
 * @param limit 查询条数
 * @return chatData列表
 */
private List<ChatDataDTO> getChatDataList(long seq, long limit) {
    long slice = Finance.NewSlice();
    int ret = Finance.GetChatData(sdk, seq, limit, "", "", 100, slice);
    if (ret != 0) {
        log.error("获取企业微信聊天记录失败,ret:【{}】", ret);
        return Collections.emptyList();
    }
    String result = Finance.GetContentFromSlice(slice);
    ChatDataResultDTO chatDataResultDTO = JSON.parseObject(result, ChatDataResultDTO.class);

    if (chatDataResultDTO.isSuccess()) {
        return chatDataResultDTO.getChatDataList();
    } else {
        log.error("获取企业微信聊天记录失败,错误码为:【{}】,错误信息为:【{}】", chatDataResultDTO.getErrCode(), chatDataResultDTO.getErrMsg());
        return Collections.emptyList();
    }
}
复制代码

解密消息

/**
 * 解析加密的消息
 *
 * @param encryptRandomKey 企业微信返回的randomKey
 * @param encryptChatMsg   加密的消息
 * @return 解密后的消息体
 */
private String decryptData(String encryptRandomKey, String encryptChatMsg) {
    try {
        String encryptKey = RSAUtil.decrypt(encryptRandomKey, privateKey);
        long slice = Finance.NewSlice();
        int ret = Finance.DecryptData(sdk, encryptKey, encryptChatMsg, slice);
        if (ret != 0) {
            log.info("解析企业微信聊天记录失败,ret:【{}】", ret);
            return "";
        }
        String text = Finance.GetContentFromSlice(slice);
        Finance.FreeSlice(slice);

        return text;
    } catch (Exception e) {
        log.error("解密企业微信聊天记录发生异常:", e);
        return "";
    }
}
复制代码

将消息转换为自己的对象

/**
 * 将chatData对象转换为chatMessage
 *
 * @param chatDataDTO ChatData对象
 * @return ChatMessage对象
 */
private ChatMessageDTO convertChatData2ChatMessage(ChatDataDTO chatDataDTO) {
    ChatMessageDTO chatMessageDTO = new ChatMessageDTO();
    chatMessageDTO.setSeq(chatDataDTO.getSeq());
    chatMessageDTO.setMsgId(chatDataDTO.getMsgId());

    String text = this.decryptData(chatDataDTO.getEncryptRandomKey(), chatDataDTO.getEncryptChatMsg());
    if (StringUtils.isNotBlank(text)) {
        JSONObject jsonObject = JSON.parseObject(text);
        chatMessageDTO.setAction(jsonObject.getString("action"));
        chatMessageDTO.setMsgType(jsonObject.getString("msgtype"));
        Long msgTimeStamp = jsonObject.getLong("msgtime");
        if (msgTimeStamp != null) {
            chatMessageDTO.setMsgTime(new Date(msgTimeStamp));
        }

        chatMessageDTO.setFrom(jsonObject.getString("from"));
        String toListStr = jsonObject.getString("tolist");
        if (StringUtils.isNotBlank(toListStr)) {
            chatMessageDTO.setToList(JSON.parseArray(toListStr, String.class));
        }
        chatMessageDTO.setRoomId(jsonObject.getString("roomid"));

        JSONObject bodyJsonObject;
        String msgType = chatMessageDTO.getMsgType();
        if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_DOC)) {
            bodyJsonObject = jsonObject.getJSONObject("doc");
        } else if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_EXTERNAL_RED_PACKET)) {
            bodyJsonObject = jsonObject.getJSONObject("redpacket");
        } else {
            bodyJsonObject = jsonObject.getJSONObject(msgType);
        }
        if (bodyJsonObject != null) {
            chatMessageDTO.setBody(bodyJsonObject.toJSONString());
        }
    }

    return chatMessageDTO;
}
复制代码

文件转存

/**
 * 转存文件
 *
 * @param msgType   消息类型
 * @param sdkFileId 文件id
 * @param subType   子类型
 * @param fileExt   扩展属性
 * @param seq       seq
 * @return 文件路径
 */
public String transferFile(String msgType, String sdkFileId, Integer subType, String fileExt, long seq) {
    if (StringUtils.isBlank(sdkFileId) || StringUtils.isBlank(msgType)) {
        return "";
    }
    if (StringUtils.isBlank(fileExt)) {
        fileExt = this.getFileExtend(msgType, subType);
    }

    String fileName = seq + "." + fileExt;
    String indexBuf = "";
    while (true) {
        long mediaData = Finance.NewMediaData();
        int ret = Finance.GetMediaData(sdk, indexBuf, sdkFileId, "", "", 60, mediaData);
        if (ret != 0) {
            log.info("获取文件失败,ret:【{}】", ret);
            return "";
        }
        try {

            this.saveToLocal(FILE_BASE_PATH + "/" + fileName, Finance.GetData(mediaData));

            if (Finance.IsMediaDataFinish(mediaData) == 1) {
                Finance.FreeMediaData(mediaData);
                // TODO: 上传至文件服务,并删除本地文件

                return FILE_BASE_PATH + "/" + fileName;
            } else {
                indexBuf = Finance.GetOutIndexBuf(mediaData);
                Finance.FreeMediaData(mediaData);
            }
        } catch (Exception e) {
            log.error("保存文件失败:", e);
            return "";
        }
    }
}

/**
 * 获取文件的扩展名
 *
 * @param msgType 消息类型
 * @param subType 类型
 * @return 扩展名
 */
private String getFileExtend(String msgType, Integer subType) {
    String extend = "";
    if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_IMAGE)) {
        extend = "png";
    } else if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_MEETING_VOICE_CALL)
            || (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_VIDEO))) {
        extend = "mp4";
    } else if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_VOICE)) {
        extend = "amr";
    } else if (Objects.equals(msgType, ChatMessageConstant.MSG_TYPE_EMOTION)) {

        if (Objects.equals(subType, 1)) {
            extend = "gif";
        } else if (Objects.equals(subType, 2)) {
            extend = "png";
        }
    }
    return extend;
}

/**
 * 保存到本地
 *
 * @param filePath 文件路径
 * @param bytes    byte数组
 * @throws IOException
 */
private void saveToLocal(String filePath, byte[] bytes) throws IOException {
    FileOutputStream outputStream = null;
    try {
        outputStream = new FileOutputStream(new File(filePath), true);
        outputStream.write(bytes);
    } finally {
        if (outputStream != null) {
            outputStream.close();
        }
    }
}
复制代码

其他说明

上面只是复制出了一些关键的代码,详细实例的github地址为:github.com/xiehuaa/wec…

实际使用中需要自行存储上次同步到的seq值。

实际使用中聊天记录落库和文件的同步进行分开处理,可以使用消息队列进行解耦,文件在本地生成后上传至云存储并删除本地文件。