最近工作中一个需求是对接企业微信,在同步聊天记录时发现企业微信的官方文档写的比较简单,有些细节在官方文档中并没有详细说明,最后费了好大的力气才搞好,今天这篇文章便来详细的记录下对接的流程,希望能对有此需求的同行提供些许的帮助。我的项目是SpringBoot
项目,在文末也会给出相关实例项目的github
地址。
下载官网提供的SDK
从官方文档下载SDK,下载下来会看到其提供了windows
和linux
两个系统的动态库文件,如下:
在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值。
实际使用中聊天记录落库和文件的同步进行分开处理,可以使用消息队列进行解耦,文件在本地生成后上传至云存储并删除本地文件。