一、配置动态库
1.企业微信官方文档下载sdk:https://open.work.weixin.qq.com/api/doc/90000/90135/91774#%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8B
2.将sdk中的libWeWorkFinanceSdk_Java.so文件上传到服务器/usr/local/lib路径,路径可自定义,爱搁哪搁哪
3.配置环境变量,修改~/.bashrc
增加 "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib,修改了路径配置的变量路径要相应修改
二、拉取数据
新建包com.tencent.wework
新建包com.tencent.wework
新建包com.tencent.wework
(重要的事说三遍)
把sdk中的Finance文件丢进去
有使用到大佬封装的jar包,github搜一下去琢磨文档
com.github.binarywang weixin-java-cp 4.1.0
解密的私钥放在工程resources/privateKey目录下
获取到回调之后去拉取存档消息
WxCpMessageArchive类是最终解析出来的数据
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.message.WxCpMessageHandler;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.*;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
@Slf4j
public class MsgauditNotifyEventHandle implements WxCpMessageHandler {
@Autowired
ResourceLoader resourceLoader;
public final Map FINANCE_MAP = new HashMap<>();
private final Map PRIVATE_KEY_MAP = new HashMap<>();
private final Map MSG_TYPE_MAP = new HashMap<>();
@PostConstruct
public void initConvertMap() throws Exception {
//检查动态库是否存在
File file = new File("/usr/local/lib/libWeWorkFinanceSdk_Java.so");
List execute = CmdUtils.execute("cat ~/.bashrc");
boolean contains = execute.contains("export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib");
if (!file.exists() || !contains){
throw new BizException("会话存档所需动态库未配置nn" +
"1.企业微信官方文档下载sdk:https://open.work.weixin.qq.com/api/doc/90000/90135/91774#%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8Bn" +
"2.将sdk中的libWeWorkFinanceSdk_Java.so文件上传到服务器/usr/local/lib路径n" +
"3.配置环境变量,修改~/.bashrc,增加 "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib"n");
}
MSG_TYPE_MAP.put("external_redpacket", "redpacket");
MSG_TYPE_MAP.put("news", "info");
MSG_TYPE_MAP.put("markdown", "info");
MSG_TYPE_MAP.put("docmsg", "doc");
}
@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map context, WxCpService wxCpService,
WxSessionManager sessionManager) {
String appId = wxMessage.getToUserName();
Integer agentId = wxMessage.getAgentId();
String key = appId + "#" + agentId;
String privateKey = PRIVATE_KEY_MAP.getOrDefault(key, null);
if (StringUtils.isBlank(privateKey)) {
try {
privateKey = this.readerFile("/privateKey/" + appId + "#" + agentId + "#private.pem");
} catch (IOException ignored) {
}
if (StringUtils.isBlank(privateKey)) {
log.error("会话存档初始化失败,未获取到解密私钥,appId:{},agentId:{}", appId, agentId);
return null;
}
PRIVATE_KEY_MAP.put(key, privateKey);
}
Long sdk = this.getSdk(appId, agentId);
if (Objects.isNull(sdk)){
return null;
}
//每次使用GetChatData拉取存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。
long slice = Finance.NewSlice();
long oldSeq = 0;
WxCpMessageArchive lastSeq = this.getLastSeq();
if (Objects.nonNull(lastSeq)) {
oldSeq = lastSeq;
}
long ret = Finance.GetChatData(sdk, oldSeq, 1000, "", "", 5, slice);
if (ret != 0) {
log.error("会话存档获取失败,appId:{},agentId:{},seq:{}", appId, agentId, oldSeq);
Finance.FreeSlice(slice);
return null;
}
String content = Finance.GetContentFromSlice(slice);
MessageResult messageResult = JSONObject.toJavaObject(JSONObject.parseObject(content), MessageResult.class);
Integer errCode = messageResult.getErrCode();
if (Objects.isNull(errCode) || errCode != 0) {
log.error("会话存档获取失败,appId:{}, agentId:{}, seq:{}, errorMsg:{}", appId, agentId, oldSeq,
messageResult.getErrMsg());
return null;
}
List chatData = messageResult.getChatData();
this.dataToQueue(chatData, sdk, slice, key, appId, agentId);
Finance.FreeSlice(slice);
return null;
}
private void dataToQueue(List chatData, long sdk, long slice, String key,
String appId, Integer agentId) {
if (CollectionUtils.isEmpty(chatData)) {
return;
}
for (MessageResult.Message chatDatum : chatData) {
String publicKeyVer = chatDatum.getPublicKeyVer();
try {
Integer seq = chatDatum.getSeq();
String encryptRandomKey = chatDatum.getEncryptRandomKey();
String encryptChatMsg = chatDatum.getEncryptChatMsg();
String decrypt = RsaUtil.pkcs1decrypt(encryptRandomKey, PRIVATE_KEY_MAP.get(key));
Finance.DecryptData(sdk, decrypt, encryptChatMsg, slice);
String message = Finance.GetContentFromSlice(slice);
JSONObject jsonObject = JSONObject.parseObject(message);
WxCpMessageArchiveVo wxCpMessageArchiveVo = JSONObject.toJavaObject(jsonObject,
WxCpMessageArchiveVo.class);
wxCpMessageArchiveVo.setContent(this.getContent(wxCpMessageArchiveVo.getMsgType(), jsonObject));
WxCpMessageArchive wxCpMessageArchive = new WxCpMessageArchive();
BeanUtils.copyProperties(wxCpMessageArchiveVo, wxCpMessageArchive);
wxCpMessageArchive.setAppId(appId);
wxCpMessageArchive.setAgentId(agentId);
wxCpMessageArchive.setSeq(seq);
wxCpMessageArchive.setPublicKeyVer(publicKeyVer);
String to = String.join(",", wxCpMessageArchiveVo.getToList());
wxCpMessageArchive.setTo(to);
Long msgTimestamp = wxCpMessageArchiveVo.getMsgTimestamp();
if (Objects.nonNull(msgTimestamp)) {
LocalDateTime msgTime = LocalDateTime.ofEpochSecond(msgTimestamp / 1000, 0, ZoneOffset.ofHours(8));
wxCpMessageArchive.setMsgTime(msgTime);
}
WxCpMessageArchiveService.MESSAGE_QUEUE.put(wxCpMessageArchive);
} catch (Exception e) {
log.error("会话入库失败:{}", chatDatum);
}
}
}
public Long getSdk(String appId, Integer agentId){
Long sdk = this.FINANCE_MAP.getOrDefault(appId + "#" + agentId, null);
if (Objects.nonNull(sdk)){
return sdk;
}
return this.initSdk(appId, agentId);
}
public Long getLastSeq(){
//获取最后一次拉取的数据的seq
return 0L;
}
private Long initSdk(String appId, Integer agentId){
WxCpConfigStorage wxCpConfigStorage = WxCpConfiguration.getCpService(agentId).getWxCpConfigStorage();
String corpId = wxCpConfigStorage.getCorpId();
String corpSecret = wxCpConfigStorage.getCorpSecret();
long sdk = Finance.NewSdk();
long ret = 0;
ret = Finance.Init(sdk, corpId, corpSecret);
if (ret != 0) {
Finance.DestroySdk(sdk);
log.error("会话存档sdk初始化失败,appId:{}, agentId:{}", appId, agentId);
return null;
}
FINANCE_MAP.put(appId + "#" + agentId, sdk);
return sdk;
}
private String getContent(String msgType, JSONObject jsonObject) {
if (!MSG_TYPE_MAP.containsKey(msgType)) {
return jsonObject.getString(msgType);
}
Object object = jsonObject.get(msgType);
if (Objects.nonNull(object)) {
return jsonObject.getString(msgType);
}
msgType = MSG_TYPE_MAP.get(msgType);
return jsonObject.getString(msgType);
}
private String readerFile(String path) throws IOException {
org.springframework.core.io.Resource resource = resourceLoader.getResource("classpath:" + path);
InputStream is = resource.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
StringBuilder stringBuilder = new StringBuilder();
String data;
while ((data = br.readLine()) != null) {
stringBuilder.append(data);
}
br.close();
isr.close();
is.close();
return stringBuilder.toString();
}
}
@Data
@NoArgsConstructor
public class MessageResult {
@JSONField(name = "errcode")
private Integer errCode;
@JSONField(name = "errmsg")
private String errMsg;
@JSONField(name = "chatdata")
private List chatData;
@Data
@NoArgsConstructor
public static class Message {
@JSONField(name = "seq")
private Integer seq;
@JSONField(name = "msgid")
private String msgId;
@JSONField(name = "publickey_ver")
private String publicKeyVer;
@JSONField(name = "encrypt_random_key")
private String encryptRandomKey;
@JSONField(name = "encrypt_chat_msg")
private String encryptChatMsg;
}
}
@Data
@NoArgsConstructor
public class WxCpMessageArchiveVo {
@JSONField(name = "msgid")
private String msgId;
private String action;
private String from;
@JSONField(name = "tolist")
private List toList;
@JSONField(name = "roomid")
private String roomId;
@JSONField(name = "msgtime")
private Long msgTimestamp;
@JSONField(name = "msgtype")
private String msgType;
@JSONField(name = "voiceid")
private String voiceId;
@JSONField(name = "voipid")
private String voipId;
private String content;
}
@Data
@NoArgsConstructor
public class WxCpMessageArchive {
private Long id;
private String appId;
private Integer agentId;
private Integer seq;
private String msgId;
private String publicKeyVer;
private String action;
private String from;
private String to;
private String roomId;
private String msgType;
private String voiceId;
private String voipId;
private String content;
private LocalDateTime msgTime;
private String filePath;
}
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class CmdUtils {
public static List execute(String command) throws IOException {
List processList = new ArrayList();
BufferedReader input = null;
try {
Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", command});
input = new BufferedReader(new InputStreamReader(process.getInputStream()));
process.getErrorStream();
String line;
while ((line = input.readLine()) != null) {
processList.add(line);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
if (input != null) {
input.close();
}
}
return processList;
}
}
import org.apache.tomcat.util.codec.binary.base64;
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.RSAPrivateCrtKeySpec;
public class RsaUtil {
public static String pkcs1decrypt(String str, String privateKey) throws Exception{
//64位解码加密后的字符串
byte[] inputByte = base64.decodebase64(str.getBytes(StandardCharsets.UTF_8));
//base64编码的私钥
PrivateKey priKey = getPkcs1PrivateKey(privateKey);
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
public static PrivateKey getPkcs1PrivateKey(String privateKey) throws Exception{
privateKey = privateKey.replaceAll("\n", "").replace("-----BEGIN RSA PRIVATE KEY-----", "").replace("-----END RSA PRIVATE KEY-----", "");
byte[] bytes = base64.decodebase64(privateKey);
DerInputStream derReader = new DerInputStream(bytes);
DerValue[] seq = derReader.getSequence(0);
BigInteger modulus = seq[1].getBigInteger();
BigInteger publicExp = seq[2].getBigInteger();
BigInteger privateExp = seq[3].getBigInteger();
BigInteger prime1 = seq[4].getBigInteger();
BigInteger prime2 = seq[5].getBigInteger();
BigInteger exp1 = seq[6].getBigInteger();
BigInteger exp2 = seq[7].getBigInteger();
BigInteger crt = seq[8].getBigInteger();
RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crt);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}
三、拉取媒体文件
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.linkedBlockingQueue;
@Service
@Slf4j
public class WxCpMessageArchiveService {
@Resource
private MsgauditNotifyEventHandle msgauditNotifyEventHandle;
//queue保存解析出来的数据,完了消费queue里的数据入库
public static final linkedBlockingQueue MESSAGE_QUEUE = new linkedBlockingQueue<>(10000);
private final List mediaTypeList = new ArrayList<>();
@PostConstruct
public void initMediaTypeList(){
mediaTypeList.add("image");
mediaTypeList.add("voice");
mediaTypeList.add("video");
mediaTypeList.add("file");
}
public void getMediaDataFromWechat(){
List archiveList = this.getNeedMediaDataList();
for (WxCpMessageArchive archive : archiveList) {
String appId = archive.getAppId();
Integer agentId = archive.getAgentId();
String msgId = archive.getMsgId();
String msgType = archive.getMsgType();
String content = archive.getContent();
JSONObject jsonObject = JSONObject.parseObject(content);
String sdkFileId = jsonObject.getString("sdkfileid");
if (StringUtils.isBlank(sdkFileId)){
continue;
}
String indexBuf = "";
Long sdk = this.msgauditNotifyEventHandle.getSdk(appId, agentId);
if (Objects.isNull(sdk)){
continue;
}
String fileName = "";
if (Objects.equals(msgType, "file")){
fileName = jsonObject.getString("filename");
}
if (StringUtils.isBlank(fileName)){
fileName = msgId;
}
String filePath = "/opt/mediaFile/" + fileName;
File file = new File(filePath);
while(true){
//每次使用GetMediaData拉取存档前需要调用NewMediaData获取一个media_data,在使用完media_data中数据后,还需要调用FreeMediaData释放。
long media_data = Finance.NewMediaData();
long ret = Finance.GetMediaData(sdk, indexBuf, sdkFileId, "", "", 5, media_data);
if(ret!=0){
log.error("会话存档, 获取媒体文件失败, msgId:{}, msgType:{}", msgId, msgType);
Finance.FreeMediaData(media_data);
//删除已拉取的媒体数据
FileUtils.deleteQuietly(file);
break;
}
try (FileOutputStream outputStream = new FileOutputStream(filePath, true);){
//大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
outputStream.write(Finance.GetData(media_data));
} catch (Exception e) {
log.error("会话存档, 写入媒体文件失败, msgId:{}, msgType:{}", msgId, msgType);
Finance.FreeMediaData(media_data);
//删除已拉取的媒体数据
FileUtils.deleteQuietly(file);
break;
}
if(Finance.IsMediaDataFinish(media_data) == 1) {
//已经拉取完成最后一个分片
Finance.FreeMediaData(media_data);
break;
} else {
//获取下次拉取需要使用的indexBuf
indexBuf = Finance.GetOutIndexBuf(media_data);
Finance.FreeMediaData(media_data);
}
}
if (file.exists()){
//媒体文件获取成功
}
}
}
public List getNeedMediaDataList(){
//获取需要拉取媒体文件的数据集
return new ArrayList<>();
}
}



