微信扫码,关注“企微云”公众号完成申请
扫码加专属顾问企业微信,我们将协助您进行线上体验!
我要投稿
企业微信可以使用会话存档,然后我们可以使用企业微信提供的sdk把会话数据获取过来。
首先去下载对应版本的jdk,我选用linux版下面的java实现。
会话存档的开启,需要填写一个RSA公钥,因此我们先要生成一个RSA密钥对,它的要求是:使用模值为2048bit的秘钥,使用PKCS1,输出格式选择PEM/Base64。
这一句话很关键,因为java默认生成的是1024位的PKCS8。
这里我在这个网站直接生成密钥对:http://web.chacuo.net/netrsakeypair
接下来是开发环节。
在服务器上新建一个lib,如/usr/local/xxx/lib,把libWeWorkFinanceSdk_Java.so放入该目录下
springboot项目启动参数加上:
-Djava.library.path=/usr/local/xxx/lib
把Finance.java放入com.tencent.wework包下。
sdk在每个线程中可以保留一个实例,一直使用,所以将初始化sdk和销毁sdk的工作放在代码最外层,即:
long sdk = SessionArchiveUtil.initSDK();try{ xxx 存档会话接口调用和业务处理}finally{ SessionArchiveUtil.destroy();}
SessionArchiveUtil:
private static ThreadLocal<Long> sdkLocal = new ThreadLocal<>();
private static void setSDK(Long sdk){
sdkLocal.set(sdk);
}
public static Long getSDK(){
return sdkLocal.get();
}
private static Long initSDK(){
ParamCheckUtil.objectNotNull(sdkLocal.get(),"sdk已经存在");
long sdk = Finance.NewSdk();
setSDK(sdk);
return sdk;
}
public static void destroy(){
if(sdkLocal.get()!= null) {
Finance.DestroySdk(sdkLocal.get());
sdkLocal.remove();
}
}
这里使用ThreadLocal来做,简单方便。
获取聊天数据(也是在SessionArchiveUtil):
private static RateLimiter limiter = RateLimiter.create(10);//每分钟不超过600次 ==> 每秒不超过10 /** * 获取存档会话数据 这里是未解密的数据 * 不可超过600次/分钟。 * @param sdk * @param seq * @param limit <=1000 * @param proxy * @param passwd * @param timeout s * @return */ public static List<SessionArchiveChatDataVO.ChatData> getChatData(Long sdk, Long seq, Long limit, String proxy, String passwd, Long timeout){ if(timeout == null){ timeout = 5L; } if(limit == null){ limit = 20L; } ParamCheckUtil.objectNull(sdk,"sdk不能为空"); ParamCheckUtil.objectNull(seq,"seq不能为空"); //每次使用GetChatData拉取存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 long slice = Finance.NewSlice(); try { limiter.acquire(); int ret = Finance.GetChatData(sdk.longValue(), seq.longValue(), limit.longValue(), proxy, passwd, timeout.longValue(), slice); checkRet(ret,"init sdk err ret" + ret); String originalDataStr = Finance.GetContentFromSlice(slice); SessionArchiveChatDataVO sessionArchiveOriginalDataVO = JSON.parseObject(originalDataStr, SessionArchiveChatDataVO.class); QYWXUtil.checkError(sessionArchiveOriginalDataVO); List<SessionArchiveChatDataVO.ChatData> originalDataList = sessionArchiveOriginalDataVO.getChatdata(); return originalDataList; }finally { Finance.FreeSlice(slice); } } /** * 专门校验Finance接口的 * @param i * @param msg */ public static void checkRet(int i,String msg){ if(i != 0){ throw new BusinessException(ResponseEnum.FAIL.getCode(),msg); } }
注意接口使用了限流。
对应的实体类:
public class SessionArchiveChatDataVO{
private Integer errcode;
private String errmsg;
private List<ChatData> chatdata;
public static class ChatData {
private Long seq;
private String msgid;//消息id,消息的唯一标识,企业可以使用此字段进行消息去重 msgid以_external结尾的消息,表明该消息是一条外部消息。msgid以_updown_stream结尾的消息,表明该消息是一条上下游消息。
private Integer publickey_ver;
private String encrypt_random_key;
private String encrypt_chat_msg;
}
}
获取到了消息,我们要对消息进行解密。还记得之前的RSA密钥对了吗,每次往企业微信里面填一次公钥,版本就加一。后面获取数据,解密的话,是需要用相应版本的私钥去解密encrypt_random_key,然后再用它用sdk去解密,得到明文消息。
首先是获得私钥:
/**
* 读取pkcs1格式的private key
* @param privKeyPEM
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(String privKeyPEM){
String privKeyPEMnew = privKeyPEM.replaceAll("\\n", "").replace("-----BEGIN RSA PRIVATE KEY-----", "").replace("-----END RSA PRIVATE KEY-----", "");
//byte[] bytes = java.util.Base64.getDecoder().decode(privKeyPEMnew);//Illegal base64 character d
byte[] bytes = org.apache.commons.codec.binary.Base64.decodeBase64(privKeyPEMnew);
try {
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 crtCoef = seq[8].getBigInteger();
RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}catch (Exception e){
throw new RuntimeException(e);
}
}
然后解密encrypt_random_key:
public static String decrptyRandomKey(PrivateKey privateKey,String encrypt_random_key) {
Cipher cipher = null;
try {
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 64位解码加密后的字符串
byte[] inputArray = Base64.decodeBase64(encrypt_random_key.getBytes("UTF-8"));
int inputLength = inputArray.length;
// 最大解密字节数,超出最大字节数需要分组加密
int MAX_ENCRYPT_BLOCK = 256;
// 标识
int offSet = 0;
byte[] resultBytes = {};
byte[] cache = {};
while (inputLength - offSet > 0) {
if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
offSet += MAX_ENCRYPT_BLOCK;
} else {
cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
offSet = inputLength;
}
resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
}
return new String(resultBytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
最后解密数据:
public static String decryptData(long sdk, String encrypt_key, String encrypt_chat_msg){ //每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 long msg = Finance.NewSlice(); try { int ret = Finance.DecryptData(sdk, encrypt_key, encrypt_chat_msg, msg); checkRet(ret,"init sdk err ret" + ret); return Finance.GetContentFromSlice(msg); }finally { Finance.FreeSlice(msg); } }
获得的这个解密内容是一个json字符串。该内容对应的实体类为:
public class SessionArchiveMsgVO {
private String msgid;//消息id,消息的唯一标识,企业可以使用此字段进行消息去重
/**
* @see SessionArchiveMsgActionEnum
*/
private String action;//消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型
private String from;//消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid
private List<String> tolist;//息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。数组
private String roomid;//群聊消息的群id。如果是单聊则为空
private Long msgtime;//消息发送时间戳,utc时间,ms单位
/**
* @see SessionArchiveMsgTypeEnum
*/
private String msgtype;
/**
* 机器人与外部联系人的账号都是external_userid,其中机器人的external_userid是以"wb"开头,
* 例如:"wbjc7bDwAAJVylUKpSA3Z5U11tDO4AAA",外部联系人的external_userid以"wo"或"wm"开头。
* 如果是机器人发出的消息,可以通过openapi拉取机器人详情:如何获取机器人详情?
* 如果是外部联系人发出的消息,可以通过openapi拉取外部联系人详情:如何获取外部联系人详情?
* 如果是引用/回复消息,发消息的用户的语言设置是中文,消息内容前面会加上“这是一条引用/回复消息:”,如果发消息的用户的语言设置是英文,消息内容的前面会加上“This is a quote/reply:”。
*/
private Long time;//action=switch才有
private String user;//action=switch才有
//msgtype = meeting_voice_call才有
private String voiceid;
private meeting_voice_call meeting_voice_call;
//msgtype=voip_doc_share 才有
private String voipid;
private voip_doc_share voip_doc_share;
//msgtype=external_redpacket才有 !! 注意,消息类型为redpacket也有这个 所以这里注释掉 留一个
//private redpacket redpacket;
//msgtype=sphfeed 才有
private Long feed_type;
private String sph_name;
private String feed_desc;
//msgtype=voiptext才有
private Long callduration;
private Long invitetype;
//msgtype=qydiskfile才有
private info info;// ? 文档不清晰
private Text text;
private Image image;
private Revoke revoke;
private Disagree disagree;
private Agree agree;
private Voice voice;
private Video video;
private Card card;
private Location location;
private Emotion emotion;
private File file;
private Link link;
private Weapp weapp;
private Chatrecord chatrecord;
private Todo todo;
private Vote vote;
private Collect collect;
private Redpacket redpacket;
private Meeting meeting;
private meeting_notification meeting_notification;
private docmsg docmsg;
private markdown markdown;
private news news;
private calendar calendar;
private mixed mixed;
public static class info {
private String filename;
}
/*@Data
public static class redpacket {
private Long type;
private String wish;
private Long totalcnt;
private Long totalamount;
}*/
public static class voip_doc_share {
private String filename;
private String md5sum;
private Long filesize;
private String sdkfileid;
}
public static class meeting_voice_call{
private Long endtime;
private String sdkfileid;
private List<demofiledata> demofiledata;
private List<sharescreendata> sharescreendata;
public static class demofiledata {
private String filename;
private String demooperator;
private Long starttime;
private Long endtime;
}
public static class sharescreendata {
private String share;
private Long starttime;
private Long endtime;
}
}
public static class Text {
private String content;
}
public static class Image {
private String md5sum;
private Long filesize;
private String sdkfileid;
}
public static class Revoke {
private String pre_msgid;
}
public static class Disagree {
private String userid;
private Long agree_time;//文档不清晰,到底是这个字段 还是下面的字段 ?
private Long disagree_time;
}
public static class Agree {
private String userid;
private Long agree_time;
}
public static class Voice {
private Long voice_size;
private Long play_length;
private String sdkfileid;
private String md5sum;
}
public static class Video {
private Long filesize;
private Long play_length;
private String sdkfileid;
private String md5sum;
}
public static class Card {
private String corpname;
private String userid;//名片所有者的id,同一公司是userid,不同公司是external_userid。String类型
}
public static class Location {
private Double longitude;
private Double latitude;
private String address;
private String title;
private Long zoom;
}
public static class Emotion {
private Integer type;//1表示gif 2表示png
private Integer width;
private Integer height;
private String sdkfileid;
private String md5sum;
private Long imagesize;
}
public static class File {
private String sdkfileid;
private String md5sum;
private String filename;
private String fileext;
private Long filesize;
}
public static class Link {
private String title;
private String description;
private String link_url;
private String image_url;
}
public static class Weapp {
private String title;
private String description;
private String username;
private String displayname;
}
public static class Chatrecord {
private String title;
private List<Item> item;
public static class Item {
private String type;//每条聊天记录的具体消息类型:ChatRecordText/ ChatRecordFile/ ChatRecordImage/ ChatRecordVideo/ ChatRecordLink/ ChatRecordLocation/ ChatRecordMixed ….
private Long msgtime;
private String content;
private Boolean from_chatroom;
}
}
public static class Todo {
private String title;
private String content;
}
public static class Vote {
private String votetitle;
private List<String> voteitem;
private Integer votetype;
private String voteid;
}
public static class Collect {
private String room_name;
private String creator;
private String create_time;
private String title;
private List<Detail> details;
public static class Detail {
private Long id;
private String ques;
private String type;//有Text(文本),Number(数字),Date(日期),Time(时间)。String类型
}
}
public static class Redpacket {
private Long type;//1 普通红包、2 拼手气群红包、3 激励群红包
private String wish;
private Long totalcnt;
private Long totalamount;
}
public static class Meeting {
private String topic;
private Long starttime;
private Long endtime;
private String address;
private String remarks;
private Long meetingid;
}
public static class meeting_notification {
private Long meetingid;
private Long notification_type;
private String content;
}
public static class docmsg {
private String title;
private String link_url;
private String doc_creator;
}
public static class markdown {
private String info;////? 啥格式
}
public static class news {
private info info;
public static class info {
private List<item> item;
public static class item {
private String title;
private String description;
private String url;
private String picurl;
}
}
}
public static class calendar {
private String title;
private String creatorname;
private List<String> attendeename;
private Long starttime;
private Long endtime;
private String place;
private String remarks;
}
public static class mixed {
private List<item> item;
public static class item {
private String type;//是上面各类消息的type
private String content;//type不同 content不同 json
}
}
}
枚举类:
public enum SessionArchiveMsgTypeEnum {
TEXT("text","文本"),
IMAGE("image","图片"),
REVOKE("revoke","撤回"),
AGREE("agree","同意"),
DISAGREE("disagree","不同意"),
VOICE("voice","语音"),
VIDEO("video","视频"),
CARD("card","名片"),
LOCATION("location","位置"),
EMOTION("emotion","表情"),
FILE("file","文件"),//
LINK("link","链接"),
WEAPP("weapp","小程序"),
CHATRECORD("chatrecord","会话记录"),
TODO("todo","待办"),
VOTE("vote","投票"),
COLLECT("collect","填表"),
REDPACKET("redpacket","红包"),
MEETING("meeting","会议邀请"),
MEETING_NOTIFICATION("meeting_notification","会议控制"),
DOCMSG("docmsg","在线文档"),
MARKDOWN("markdown","MarkDown"),
NEWS("news","图文"),
CALENDAR("calendar","日程"),
MIXED("mixed","混合类型"),
MEETING_VOICE_CALL("meeting_voice_call","音频存档"),
VOIP_DOC_SHARE("voip_doc_share","音频共享文档"),
EXTERNAL_REDPACKET("external_redpacket","互通红包"),
SPHFEED("sphfeed","视频号"),
VOIPTEXT("voiptext","音视频通话"),
QYDISKFILE("qydiskfile","微盘文件"),
;
private String code;
private String msg;
public static String getNameByCode(String code){
SessionArchiveMsgTypeEnum[] enums = SessionArchiveMsgTypeEnum.values();
for(int i = 0; i < enums.length; i++){
SessionArchiveMsgTypeEnum anEnum = enums[i];
if(anEnum.getCode().equalsIgnoreCase(code)){
return anEnum.getMsg();
}
}
return null;
}
}
public enum SessionArchiveMsgActionEnum {
SEND("send","发送消息"),
RECALL("recall","撤回消息"),
SWITCH("switch","切换企业日志"),
;
private String code;
private String msg;
public static String getNameByCode(String code){
SessionArchiveMsgActionEnum[] enums = SessionArchiveMsgActionEnum.values();
for(int i = 0; i < enums.length; i++){
SessionArchiveMsgActionEnum anEnum = enums[i];
if(anEnum.getCode().equalsIgnoreCase(code)){
return anEnum.getMsg();
}
}
return null;
}
}
获取媒体文件:
/**
* 获取媒体文件
* @param sdkfileid
* @param proxy
* @param passwd
* @param timeout
* @param savefile //绝对路径,一直到文件名
*/
public static void GetMediaData(Long sdk,String sdkfileid,String proxy,String passwd,Long timeout,String savefile){
//拉取媒体文件
if(timeout == null){
timeout = 5L;
}
ParamCheckUtil.stringEmpty(sdkfileid,"sdkfileid不能为空");
ParamCheckUtil.stringEmpty(savefile,"savefile不能为空");
ParamCheckUtil.objectNull(sdk,"sdk不能为空");
//媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。
//indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“,表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
String indexbuf = "";
while(true) {
//每次使用GetMediaData拉取存档前需要调用NewMediaData获取一个media_data,在使用完media_data中数据后,还需要调用FreeMediaData释放。
long media_data = Finance.NewMediaData();
try {
int ret = Finance.GetMediaData(sdk.longValue(), indexbuf, sdkfileid, proxy, passwd, timeout.longValue(), media_data);
checkRet(ret,"getmediadata ret:" + ret);
log.info("getmediadata outindex len:{}, data_len:{}, is_finis:{}\n",Finance.GetIndexLen(media_data),Finance.GetDataLen(media_data), Finance.IsMediaDataFinish(media_data));
//大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
FileOutputStream outputStream = new FileOutputStream(new File(savefile), true);
outputStream.write(Finance.GetData(media_data));
outputStream.close();
if(Finance.IsMediaDataFinish(media_data) == 1) {
//已经拉取完成最后一个分片
break;
} else {
//获取下次拉取需要使用的indexbuf
indexbuf = Finance.GetOutIndexBuf(media_data);
}
} catch(Exception e){
throw new RuntimeException(e);
}finally {
Finance.FreeMediaData(media_data);
}
}
}
对于使用sdk,可能会存在报错,甚至直接导致jvm崩溃:
A fatal error has been detected by the Java Runtime Environment:## SIGSEGV (0xb) at pc=0x00007f65e5bdcdcd, pid=19004, tid=0x00007f65e75fd700
首先是要确保每个初始化获得的sdk,不存在并发调用的情况。也就是比如初始化一个sdk=100的量,在线程A和线程B之间都在并发调用,这是不允许的,只允许一个线程去使用和释放。
排除了这个,依旧报错,可以增大jvm堆内存和线程栈大小:
#!/bin/bashnohup java -Xmx512m -Xmn256m -Xss20m -jar -Dspring.profiles.active=prd -Dlogging.config=./logback-prd.xml -Djava.library.path=/usr/local/xxx/lib xxx-0.0.1-SNAPSHOT.jar > console.log 2>&1 &tail -f console.log
特别是线程栈大小。猜测是由于一次要处理多条数据,且是2048位的RSA,对内存占用很大。
但是如果继续上调线程栈大小,也不可取。因为tomcat本身有很多线程、spring有内置的很多线程、还有redis连接线程、数据库连接等等。这些线程栈都变成30m或者50m的话,内存很快就会消耗光。因此最好是使用线程池,只要是跟会话存档sdk打交道的地方,都用线程池去执行。
定义线程池:
private synchronized static ThreadPoolExecutor getThreadPool(){
if(ThreadPool == null){
//使用线程池的目的,是由于企业微信的sdk,需要设置线程栈大小 但是统一调大了太耗费内存
ArrayBlockingQueue runnableTaskQueue = new ArrayBlockingQueue(4096);
ThreadFactory threadFactory = new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(null,r,"qw_session_archive_",1024 * 1024 * 50);//50M
}
};
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
//核心线程和最大线程设置为1
ThreadPool = new ThreadPoolExecutor(1, 1, 50,
TimeUnit.MINUTES,runnableTaskQueue,threadFactory, rejectedExecutionHandler);
}
return ThreadPool;
}
封装任务:
public class SessionArchiveChatDataDecryptCallable implements Callable<String> {
private SessionArchiveChatData encryptData;
private Long sdk;
public SessionArchiveChatDataDecryptCallable(SessionArchiveChatData encryptData,Long sdk){
this.encryptData = encryptData;
this.sdk = sdk;
}
public String call() throws Exception {
SessionArchiveMapper sessionArchiveMapper = ApplicationContextHolder.getBean(SessionArchiveMapper.class);
SessionArchive sessionArchive = sessionArchiveMapper.selectOne(new LambdaQueryWrapper<SessionArchive>()
.eq(SessionArchive::getCorpId,encryptData.getCorpId())
.eq(SessionArchive::getVersion,encryptData.getPublickeyVer())
);
ParamCheckUtil.objectNull(sessionArchive, "版本" + encryptData.getPublickeyVer() + "的对应公钥私钥没有" );
String privateKeyStr = sessionArchive.getPrivateKey();
ParamCheckUtil.stringEmpty(privateKeyStr,"私钥为空");
PrivateKey privateKey = SessionArchiveUtil.getPrivateKey(privateKeyStr);
String encrypt_key = SessionArchiveUtil.decrptyRandomKey(privateKey, encryptData.getEncryptRandomKey());
String decryptMsg = SessionArchiveUtil.decryptData(sdk,encrypt_key,encryptData.getEncryptChatMsg());
return decryptMsg;
}
}
提交到线程池:
public static Future<String> submit(SessionArchiveChatDataDecryptCallable callable){ return getThreadPool().submit(callable); }
53AI,让企业变得更强大
53AI公司的创始人杨芳贤曾是一位资深开源软件开发者,也是腾讯的TVP(腾讯云最具价值专家)。2014年,他的开源软件IBOS(全球用户量最大的开源协同办公平台)被开源中国评为最受欢迎的开源软件。2017年,他带领团队开发了首个微信小名片,初心是帮助成千上万与他一样的商务人士能更高效的见面、更高效的建立信任、更高效的完成价值传递。
2023年3月,他带领团队推出了53AI,至今已成功帮助上百家企业落地应用大模型,服务了包括中国联通、拉卡拉、开元教育、英氏控股、英迈中国、海尔金融、31会议、销售罗盘、昆仑学堂等数百家中大型企业。
2024-10-30
2024-11-06
2024-11-13
2024-11-06
2024-11-13
2024-11-13
2024-11-06
2024-10-30
2024-10-30
2024-10-30
2024-11-21
2024-11-20
2024-11-20
2024-11-20
2024-11-20
2024-11-20
2024-11-19
2024-11-18
描述11
按钮文案-