Bootstrap

【SpringBoot实现企业微信会话内容存档】linux部署

注:本文是在windows环境下开发完成的。linux环境还没进行测试。


前言

为保障客户服务质量、提高内部协作效率和监管合规等原因,企业微信提供会话内容存档功能。企业可以统一设置存档的员工范围,并通过API获取开启存档员工的工作沟通内容,满足企业的外部监管合规和内部管理需求。


一、使用前帮助

1、管理后台开启会话内容存档,开启前需设置开启范围、IP地址及消息加密公钥(注:员工的企业微信需升级到2.8.9版本及以上),开启后:

①开启存档的员工登录客户端,进入企业后会经过告知页面,获知后可继续使用
②企业可以获取开启存档的员工和未开启员工之间的会话内容,但不能获取未开启员工之间的会话内容
2、会话内容存档功能由企业管理员进行设置。出于安全考虑,每次操作前需要通过声纹验证管理员身份(需保证已在微信内设置声纹),验证通过后才能进行操作。每次身份验证有效期为30分钟。
在这里插入图片描述
3、设置公钥,需要在管理端配置消息加密公钥,这是用于加密和解密聊天记录的,非常重要。密钥的版本号,每更新一次,版本号就会+1,版本号变更,之前的消息就解密不了了。个人建议不要经常更换,若要更换也要把历史秘钥对保存起来,切记切记!!!。本人就在这里吃了亏。
在这里插入图片描述
密钥对可以通过在线网址生成:在线生成RSA私钥对
在这里插入图片描述
公钥配置在管理端,私钥配置在代码里。

二、获取会话内容

1.下载SDK

先下载企业微信提供的sdk
linux环境 SDK:
下载 SDK v1.2 [更新时间:2020-11-16 更新特性:更新sdk示例项目]
下载 SDK v1.1 [更新时间:2020-04-01 更新特性:支持并发调用]
下载 SDK v1.0
windows环境 SDK:
下载 SDK v1.1[更新时间:2020-12-23 更新特性:支持并发调用、更新sdk示例项目]
下载 SDK v1.0

2.需要引入的jar包

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpg-jdk16</artifactId>
            <version>1.46</version>
            <exclusions>
                <exclusion>
                    <groupId>org.bouncycastle</groupId>
                    <artifactId>bcprov-jdk16</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.64</version>
        </dependency>

3.开发环境说明

使用SpringBoot开发时,我是使用Windows系统,将sdk中的.dll文件直接放入C:\Windows\System32目录下,然后在环境变量的Path中添加该目录。
在这里插入图片描述
如果是linux系统,先将 libWeWorkFinanceSdk_Java.so文件上传到服务器上自己创建的lib目录下,也可以使用系统的lib,但是不推荐(容易找不着),然后再linux环境启动项目时增加启动命令:

-Djava.library.path=这里填写文件保存的目录

(如果配置到全局环境变量中也可以不增加启动命令)
这种办法可以参考链接https://www.pudn.com/news/625e86708cbeb85d5722c955.html

也可以直接配置LD_LIBRARY_PATH环境变量

export LD_LIBRARY_PATH=这里填写文件保存的目录 :$LD_LIBRARY_PATH
export LIBRARY_PATH=这里填写文件保存的目录 :$LIBRARY_PATH

注意:sdk中的Finace.java文件必须放入项目的com.tencent.wework目录下,切记
在这里插入图片描述

4.linux部署

Linux部署时,项目打包切记要改Finance类的静态加载代码块

    static {
        System.loadLibrary("WeWorkFinanceSdk_Java");
    }

之前windows的是没有后缀_java的,linux的有。我在这找了好久的问题。

5.开发,代码实现

第一步:初始化SDK,拿到的sdkid放入了缓存当中

    /**
     * 第一步:初始化sdk,获取sdk对象,首次使用初始化
     * 第二步:初始化函数,return 返回是否初始化成功 0:-成功,!=0:-失败
     * @param sdkId  sdkid
     * @param corpid 应用id
     * @param secrectkey 密钥
     * @return
     */
    @Override
    public Result initSdk(String sdkId, String corpid, String secrectkey) {
        long ret;
        Long sdk = Finance.NewSdk();
        ret = Finance.Init(sdk, corpid, secrectkey);
        if(ret != 0){
            Finance.DestroySdk(sdk);
            sdkMap.remove(sdkId);
            return new Result(ret,"init sdk err",null);
        }
        if(StrUtil.isNotBlank(sdkId)){
            sdkMap.put(sdkId,sdk);
            return Result.success("init sdk success",sdkId);
        }else{
            sdkMap.put(String.valueOf(sdk),sdk);
            return Result.success("init sdk success",String.valueOf(sdk));
        }
    }

第二部:拉取会话数据,这里是没有解密的

    /**
     * 拉取会话数据
     * @param sdkId sdkId
     * @param seq 会话seq
     * @param limit 会话条数
     * @param proxy 代理名
     * @param passwd 代理密码
     * @param timeout 超时时间
     * @return
     */
    @Override
    public Result getChatData(String sdkId, int seq, int limit, String proxy, String passwd, int timeout){
        Long sdk = sdkMap.get(sdkId);
        if(ObjectUtil.isNull(sdk)){
            return Result.error("init sdk err");
        }
        log.info("获取GetChatData参数:sdk:{},seq:{},limit:{},proxy:{}",sdk,seq,limit,proxy);
        //每次使用GetChatData拉取存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。
        long slice = Finance.NewSlice();
        long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
        if (ret != 0) {
            log.error("获取GetChatData失败:{}",ret);
            Finance.FreeSlice(slice);
            return new Result(ret,"获取GetChatData失败",null);
        }
        String result = Finance.GetContentFromSlice(slice);
        log.info("获取GetChatData成功:{}",result);
        Finance.FreeSlice(slice);
        return Result.success("获取GetChatData成功",result);
    }

第三步:解密会话数据

    /**
     * 解密会话存档内容
     * @param sdkId
     * @param pkv 私有-加密此条消息使用的公钥版本号。Uint32类型
     * @param encryptRandomKey 使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。String类型
     * @param encryptChatMsg-消息密文。需要业务方使用将encrypt_random_key解密得到的内容,与encrypt_chat_msg,传入sdk接口DecryptData,得到消息明文。String类型
     * @return
     */
    @Override
    public Result decryptData(String sdkId, String pkv,String encryptRandomKey, String encryptChatMsg) {
        Long sdk = sdkMap.get(sdkId);
        if(ObjectUtil.isNull(sdk)){
            return Result.error("init sdk err");
        }
        String encryptKey = RSAUtil.decrypt(encryptRandomKey,pkv);
        return decryptData(sdkId,encryptKey,encryptChatMsg);
    }

RSAUtil.decrypt(encryptRandomKey,pkv);方法

    /**
     * RSA私钥解密
     * @param str 加密字符串
     * @param privateKey 私钥
     * @return 铭文
     * @throws Exception
     * 解密过程中的异常信息
     */
    public static String decrypt(String str, String privateKey){
        log.info("密文:{}",str);
        String outStr = null;
        try {
            outStr = RSAUtil.getPrivateKeyByPKCS1(str,privateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return outStr;
    }

    /**
     * 用此方法先获取秘钥
     * RSAUtil_PKCS1
     * @param encrypt_random_key
     * @param privKeyPEM
     * @return
     * @throws Exception
     */
    public static String getPrivateKeyByPKCS1(String encrypt_random_key,String privKeyPEM) throws Exception {
        byte[] bytes = Base64.decodeBase64(privKeyPEM);
        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);
        // 64位解码加密后的字符串
        byte[] inputByte = Base64.decodeBase64(encrypt_random_key.getBytes("UTF-8"));
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        //RSA解密
        //当长度过长的时候,需要分割后解密 128个字节
        String outStr = new String(getMaxResultDecrypt(encrypt_random_key, cipher));
        log.info("RSA私钥解密后的数据|outStr:{}", outStr);
        return outStr;
    }

    /**
     *
     * @param str
     * @param cipher
     * @return
     */
    private static byte[] getMaxResultDecrypt(String str, Cipher cipher) throws Exception {
        byte[] inputArray = Base64.decodeBase64(str.getBytes("UTF-8"));
        int inputLength = inputArray.length;
        log.info("解密字节数|inputLength:{}", inputLength);
        // 最大解密字节数,超出最大字节数需要分组加密
        int MAX_ENCRYPT_BLOCK = 128;
        // 标识
        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 resultBytes;
    }

decryptData(sdkId,encryptKey,encryptChatMsg);方法

    /**
     * 解密会话存档内容
     * @param sdkId
     * @param encryptKey
     * @param encryptChatMsg
     * @return
     */
    @Override
    public Result decryptData(String sdkId, String encryptKey, String encryptChatMsg) {
        Long sdk = sdkMap.get(sdkId);
        if(ObjectUtil.isNull(sdk)){
            return Result.error("init sdk err");
        }
        long msg = Finance.NewSlice();
        long ret = Finance.DecryptData(sdk, encryptKey, encryptChatMsg, msg);
        if (ret != 0) {
            System.out.println("decryptData ret " + ret);
            Finance.FreeSlice(msg);
            return new Result(ret,"decryptData err",null);
        }
        String result = Finance.GetContentFromSlice(msg);
        System.out.println("decryptData ret:" + ret + " msg:" + result);
        Finance.FreeSlice(msg);
        return Result.success("decryptData success",result);
    }

我的Result是自定义的一个返回范类

package com.util;

import cn.hutool.core.util.ObjectUtil;

import java.util.HashMap;

public class Result extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    public static final String CODE_TAG = "code";

    public static final String MSG_TAG = "msg";

    public static final String DATA_TAG = "data";

    /**
     * 状态类型
     */
    public enum Type
    {
        /** 成功 */
        SUCCESS(0L),
        /** 警告 */
        WARN(301L),
        /** 错误 */
        ERROR(500L);
        private final long value;

        Type(long value)
        {
            this.value = value;
        }

        public long value()
        {
            return this.value;
        }
    }

    /** 状态类型 */
    private Type type;

    /** 状态码 */
    private long code;

    /** 返回内容 */
    private String msg;

    /** 数据对象 */
    private Object data;

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public Result()
    {
    }

    /**
     * 初始化一个新创建的 Result 对象
     *
     * @param type 状态类型
     * @param msg 返回内容
     */
    public Result(Type type, String msg)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 Result 对象
     *
     * @param type 状态类型
     * @param msg 返回内容
     * @param data 数据对象
     */
    public Result(Type type, String msg, Object data)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
        if (ObjectUtil.isNotEmpty(data))
        {
            super.put(DATA_TAG, data);
        }
    }


    /**
     * 初始化一个新创建的 Result 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public Result(long code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (ObjectUtil.isNotEmpty(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static Result success()
    {
        return Result.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static Result success(Object data)
    {
        return Result.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static Result success(String msg)
    {
        return Result.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static Result success(String msg, Object data)
    {
        return new Result(Type.SUCCESS, msg, data);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static Result warn(String msg)
    {
        return Result.warn(msg, null);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static Result warn(String msg, Object data)
    {
        return new Result(Type.WARN, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static Result error()
    {
        return Result.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static Result error(String msg)
    {
        return Result.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static Result error(String msg, Object data)
    {
        return new Result(Type.ERROR, msg, data);
    }

    public Type getType()
    {
        return type;
    }

    public void setType(Type type)
    {
        this.type = type;
    }

    public long getCode()
    {
        return (long) this.get("code");
    }

    public void setCode(long code)
    {
        this.code = code;
    }

    public String getMsg()
    {
        return (String) this.get("msg");
    }

    public void setMsg(String msg)
    {
        this.msg = msg;
    }

    public Object getData()
    {
        return this.get("data");
    }

    public void setData(Object data)
    {
        this.data = data;
    }
}

第四个:获取媒体文件

    /**
     * 拉取媒体文件
     * @return
     */
    @Override
    public void getMediaDataAndUpload(String sdkId, String msgtype, JSONObject jsonObject,String proxy,String passwd,int timeout) {
        Map<String,Object> map = new HashMap<>();
        Long sdk = sdkMap.get(sdkId);
        if(ObjectUtil.isNull(sdk)){
            throw new RuntimeException("init sdk err");
        }
        try {
            String savefileName = "";
            JSONObject file = new JSONObject();
            if (!jsonObject.isNull("msgid")) {
                file = jsonObject.getJSONObject(msgtype);
                savefileName = jsonObject.getStr("msgid");
            } else {
                // 混合消息
                file = jsonObject;
                savefileName = file.getStr("md5sum");
            }
            log.info("媒体文件消息:{}",file.toString());
            /* ============ 文件存储目录及文件名 Start ============ */
            String suffix = "";
            switch (msgtype) {
                case "image" : suffix = ".jpg"; break;
                case "voice" : suffix = ".amr"; break;
                case "video" : suffix = ".mp4"; break;
                case "emotion" :
                    int type = (int) file.get("type");
                    if (type == 1) {
                        suffix = ".gif";
                    } else if (type == 2) {
                        suffix = ".png";
                    }
                    break;
                case "file" :
                    suffix = "." + file.getStr("fileext");
                    break;
            }
            savefileName += suffix;
            String path = basePath;
            String savefile = path + savefileName;
            File targetFile = new File(savefile);
            if (!targetFile.getParentFile().exists()){
                //创建父级文件路径
                targetFile.getParentFile().mkdirs();
            }
            /* ============ 文件存储目录及文件名 End ============ */

            /* ============ 拉去文件 Start ============ */
            int i = 0; boolean isSave = true;
            String indexbuf = "";
            String sdkfileid = file.getStr("sdkfileid");
            while (true) {
                long mediaData = Finance.NewMediaData();
                int ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData);
                if (ret != 0) {
                    log.info("getmediadata ret:{}",ret);
                    Finance.FreeMediaData(mediaData);
                    return null;
                }
                log.info("getmediadata outindex len:{}, data_len:{}, is_finis:{}", Finance.GetIndexLen(mediaData), Finance.GetDataLen(mediaData), Finance.IsMediaDataFinish(mediaData));
                try {
                    // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
                    FileOutputStream outputStream = new FileOutputStream(new File(savefile), true);
                    outputStream.write(Finance.GetData(mediaData));
                    outputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                if (Finance.IsMediaDataFinish(mediaData) == 1) {
                    // 已经拉取完成最后一个分片
                    Finance.FreeMediaData(mediaData);
                    break;
                } else {
                    // 获取下次拉取需要使用的indexbuf
                    indexbuf = Finance.GetOutIndexBuf(mediaData);
                    Finance.FreeMediaData(mediaData);
                }
                // 若文件大于50M则不保存
                if (++i > 100) {
                    isSave = false;
                    break;
                }
            }
            /* ============ 拉去文件 End ============ */
            if (isSave) {
                file.set("sdkfileid", savefile);
                File file1 = new File(savefile);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

第五步:释放sdk,当本次拉取结束时,可以选择调用释放sdk

    /**
     * 释放sdk
     * @param sdkId sdkid
     * @return
     */
    @Override
    public Result destroySdk(String sdkId) {
        Long sdk = sdkMap.get(sdkId);
        if(ObjectUtil.isNull(sdk)){
            return Result.error("init sdk err");
        }
        Finance.DestroySdk(sdk);
        sdkMap.remove(sdkId);
        return Result.success("destroySdk success");
    }

以上都是加密解密调用的方法,拉取存库实现的逻辑还得自己来写,直接上代码

    private String[] msgtypeStr = {"image", "voice", "video", "emotion", "file"};

    @Override
    public Result getMsgSave() {
        //初始化sdk
        iTxChatService.initSdk(SDKID,corpid,secrectkey);
        //获取会话数据,这里我是把seq的值存入了数据库,这里你们自行存库存redis存内存当中都行
        //代表消息的seq值,标识消息的序号。再次拉取需要带上上次回包中最大的seq。
        String sqlV = tWxSysconfigMapper.getTWxSysconfig(AppConstant.RECORD_SEQ);
        int seq = Integer.valueOf(sqlV);
        int limit = 1000;
        String proxy = "http://" + httpProxyHost + ":" + httpProxyPort;
        List<String> msgTypeList = Arrays.asList(msgtypeStr);
        int a = 1;
        while(true){
            log.info("******第{}次拉取开始*****",a);
            Result data = iTxChatService.getChatData(SDKID, seq, limit, proxy, null, 10);
            if(data.getCode() == Result.Type.SUCCESS.value()){
            	//拿到了会话数据,此时还没有解密
            	//样例{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]}
                JSONObject chatData = JSONUtil.parseObj(data.getData());
                JSONArray jsonArray = chatData.getJSONArray("chatdata");
                if(!jsonArray.isEmpty()){
                    //遍历
                    for (int i = 0; i < jsonArray.size() ; i++) {
                        seq = jsonArray.getJSONObject(i).getInt("seq");
                        //这里记录着seq
                        seqMap.put("seq",seq);
                        JSONObject chatOne = jsonArray.getJSONObject(i);
                        log.info("密钥信息:{}",chatOne.toString());
                        try {
                        	//公钥版本号,我这里是到了第四个版本,之前的版本不记得了,所以之前版本的消息都不解密了
                            if(chatOne.getInt("publickey_ver") != 4) {
                                continue;
                            }
                            //解密,pk是放在代码当中的私钥,
                            Result dcRes = iTxChatService.decryptData(SDKID,pk,chatOne.getStr("encrypt_random_key"),chatOne.getStr("encrypt_chat_msg"));
                            if(dcRes.getCode() == Result.Type.SUCCESS.value()){
                                log.info("解密数据:{}",dcRes.getData());
                                //解密数据
                                JSONObject res = JSONUtil.parseObj(dcRes.getData());
								//res就是解密之后的数据,各位可以自行判断消息类型来处理想要的消息
								//res.getStr("msgtype")
                                //文本消息
                                if("text".equals(res.getStr("msgtype"))){
                                    System.out.println(res.getJSONObject("text").getStr("content"));
                                }else if(msgTypeList.contains(res.getStr("msgtype"))){
                                    //处理媒体消息,上面有拉取媒体消息的方法,自行处理
                                    Map<String,Object> map = iTxChatService.getMediaDataAndUpload(SDKID,res.getStr("msgtype"),res,proxy,null,10);
                                    System.out.println(com.alibaba.fastjson.JSONObject.toJSONString(map));
                                }else if("revoke".equals(res.getStr("msgtype"))){
                                    //撤回消息
                                    System.out.println(res.getJSONObject("revoke").getStr("pre_msgid"));
                                }else if("agree".equals(res.getStr("msgtype"))){
                                    //同意会话聊天内容
                                    System.out.println(res.getJSONObject("agree").toString());
                                }else if("disagree".equals(res.getStr("msgtype"))){
                                    //不同意消息
                                    System.out.println(res.getJSONObject("disagree").toString());
                                }else if("card".equals(res.getStr("msgtype"))){
                                    //名片
                                    System.out.println(res.getJSONObject("card").toString());
                                }else if("location".equals(res.getStr("msgtype"))){
                                    //位置
                                    System.out.println(res.getJSONObject("location").toString());
                                }else if("link".equals(res.getStr("msgtype"))){
                                    //链接
                                    System.out.println(res.getJSONObject("link").toString());
                                }else if("weapp".equals(res.getStr("msgtype"))){
                                    //小程序
                                    System.out.println(res.getJSONObject("weapp").toString());
                                }else if("chatrecord".equals(res.getStr("msgtype"))){
                                    //会话记录消息
                                    System.out.println(res.getJSONObject("chatrecord").toString());
                                }else if("todo".equals(res.getStr("msgtype"))){
                                    //代办消息
                                    System.out.println(res.getJSONObject("todo").toString());
                                }else if("vote".equals(res.getStr("msgtype"))){
                                    //投票消息
                                    System.out.println(res.getJSONObject("vote").toString());
                                }else if("collect".equals(res.getStr("msgtype"))){
                                    //填表消息
                                    System.out.println(res.getJSONObject("collect").toString());
                                }else if("redpacket".equals(res.getStr("msgtype"))){
                                    //红包消息
                                    System.out.println(res.getJSONObject("redpacket").toString());
                                }else if("meeting".equals(res.getStr("msgtype"))){
                                    //会议邀请消息
                                    System.out.println(res.getJSONObject("meeting").toString());
                                }else if("docmsg".equals(res.getStr("msgtype"))){
                                    //在线文档消息
                                    System.out.println(res.getJSONObject("doc").toString());
                                }else if("markdown".equals(res.getStr("msgtype"))){
                                    //MarkDown格式消息
                                    System.out.println(res.getJSONObject("info").toString());
                                }else if("news".equals(res.getStr("msgtype"))){
                                    //图文消息
                                    System.out.println(res.getJSONObject("info").toString());
                                }else if("calendar".equals(res.getStr("msgtype"))){
                                    //图文消息
                                    System.out.println(res.getJSONObject("calendar").toString());
                                }else if("mixed".equals(res.getStr("msgtype"))){
                                    //混合消息
                                    JSONObject mixed = res.getJSONObject("mixed");
                                    JSONArray jsonArray1 = mixed.getJSONArray("item");
                                    if(!jsonArray1.isEmpty()){
                                        for (int j = 0; j < jsonArray1.size(); j++) {
                                            JSONObject object = jsonArray1.getJSONObject(j);
                                            if(msgTypeList.contains(object.getStr("type"))){
                                                //处理媒体消息
                                                JSONObject object1 = JSONUtil.parseObj(object.getStr("content"));
                                                Map<String,Object> map = iTxChatService.getMediaDataAndUpload(SDKID,object.getStr("type"),object1,proxy,null,10);
                                                object.set("content",map);
                                                jsonArray1.set(j,object);
                                            }
                                        }
                                    }
                                    mixed.set("item",jsonArray1);
                                    System.out.println(mixed.toString());
                                }
                            }
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                    //更新SEQ
                    tWxSysconfigMapper.updateTWxSysconfig(seqMap.get("seq").toString(),AppConstant.RECORD_SEQ);
                }else{
                    //当前消息已拉完
                    break;
                }
            }else{
                break;
            }
            log.info("******第{}次拉取完毕*****",a);
            a++;
        }
        log.info("******会话拉取结束******");
        return Result.success("会话拉取结束");
    }

开发就已经基本完成了,部署的话我现在还没有测试,等到时候测试好了如果还有坑到时候我会补上。
以上我本地运行是可以获取到解密消息的。有兴趣的小伙伴可以去试试。


总结

以下是开发时参考的文章:
https://blog.csdn.net/weixin_48908035/article/details/124119947
个人感觉微信为什么不直接出api接口来调用,整个sdk坑的很

;