Browse Source

:new: #1596 【企业微信】新增会话存档相关接口

Wong 3 years ago
parent
commit
c0535f87fb
19 changed files with 2308 additions and 13 deletions
  1. 4 0
      spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java
  2. 12 13
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
  3. 207 0
      weixin-java-cp/src/main/java/com/tencent/wework/Finance.java
  4. 108 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
  5. 7 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
  6. 6 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
  7. 196 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
  8. 65 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpAgreeInfo.java
  9. 68 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpChatDatas.java
  10. 987 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpChatModel.java
  11. 57 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpCheckAgreeRequest.java
  12. 39 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpFileItem.java
  13. 61 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpGroupChat.java
  14. 7 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
  15. 6 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
  16. 6 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
  17. 6 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
  18. 33 0
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java
  19. 433 0
      weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java

+ 4 - 0
spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java

@@ -38,6 +38,10 @@ public class WxCpProperties {
    * 微信企业号应用 EncodingAESKey
    */
   private String aesKey;
+  /**
+   * 微信企业号应用 会话存档类库路径
+   */
+  private String msgAuditLibPath;
 
   /**
    * 配置存储策略,默认内存

+ 12 - 13
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java

@@ -1,19 +1,6 @@
 package me.chanjar.weixin.common.util.crypto;
 
-import java.io.StringReader;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Random;
-import javax.crypto.Cipher;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
 import com.google.common.base.CharMatcher;
-import com.google.common.io.BaseEncoding;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import me.chanjar.weixin.common.error.WxRuntimeException;
@@ -22,6 +9,18 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xml.sax.InputSource;
 
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Random;
+
 /**
  * <pre>
  * 对公众平台发送给公众账号的消息加解密示例代码.

+ 207 - 0
weixin-java-cp/src/main/java/com/tencent/wework/Finance.java

@@ -0,0 +1,207 @@
+package com.tencent.wework;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 注意:
+ * 此类必须配置在com.tencent.wework路径底下,否则会报错:
+ * java.lang.UnsatisfiedLinkError: com.xxx.Finance.NewSdk()
+ * <p>
+ * Q:JAVA版本的sdk报错UnsatisfiedLinkError?
+ * A:请检查是否修改了sdk的包名。
+ * <p>
+ * 官方文档:
+ * https://developer.work.weixin.qq.com/document/path/91552
+ *
+ * @author Wang_Wong
+ * @date 2022-01-17
+ */
+@Slf4j
+public class Finance {
+
+  private static volatile long sdk = -1L;
+  private static Finance finance = null;
+  private static final String SO_FILE = "so";
+  private static final String DLL_FILE = "dll";
+
+  public native static long NewSdk();
+
+  /**
+   * 初始化函数
+   * Return值=0表示该API调用成功
+   *
+   * @param [in] sdk			NewSdk返回的sdk指针
+   * @param [in] corpid      调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
+   * @param [in] secret		聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
+   * @return 返回是否初始化成功
+   * 0   - 成功
+   * !=0 - 失败
+   */
+  public native static int Init(long sdk, String corpid, String secret);
+
+  /**
+   * 拉取聊天记录函数
+   * Return值=0表示该API调用成功
+   *
+   * @param [in]  sdk				NewSdk返回的sdk指针
+   * @param [in]  seq				从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+   * @param [in]  limit			一次拉取的消息条数,最大值1000条,超过1000条会返回错误
+   * @param [in]  proxy			使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+   * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+   * @param [out] chatDatas		返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。
+   * @return 返回是否调用成功
+   * 0   - 成功
+   * !=0 - 失败
+   */
+  public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
+
+  /**
+   * 拉取媒体消息函数
+   * Return值=0表示该API调用成功
+   *
+   * @param [in]  sdk				NewSdk返回的sdk指针
+   * @param [in]  sdkFileid		从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid
+   * @param [in]  proxy			使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+   * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+   * @param [in]  indexbuf		媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。
+   * @param [out] media_data		返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记)
+   * @return 返回是否调用成功
+   * 0   - 成功
+   * !=0 - 失败
+   */
+  public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
+
+  /**
+   * @param [in]  encrypt_key, getchatdata返回的encrypt_key
+   * @param [in]  encrypt_msg, getchatdata返回的content
+   * @param [out] msg, 解密的消息明文
+   * @return 返回是否调用成功
+   * 0   - 成功
+   * !=0 - 失败
+   * @brief 解析密文
+   */
+  public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
+
+  public native static void DestroySdk(long sdk);
+
+  public native static long NewSlice();
+
+  /**
+   * @return
+   * @brief 释放slice,和NewSlice成对使用
+   */
+  public native static void FreeSlice(long slice);
+
+  /**
+   * @return 内容
+   * @brief 获取slice内容
+   */
+  public native static String GetContentFromSlice(long slice);
+
+  /**
+   * @return 内容
+   * @brief 获取slice内容长度
+   */
+  public native static int GetSliceLen(long slice);
+
+  public native static long NewMediaData();
+
+  public native static void FreeMediaData(long mediaData);
+
+  /**
+   * @return outindex
+   * @brief 获取mediadata outindex
+   */
+  public native static String GetOutIndexBuf(long mediaData);
+
+  /**
+   * @return data
+   * @brief 获取mediadata data数据
+   */
+  public native static byte[] GetData(long mediaData);
+
+  public native static int GetIndexLen(long mediaData);
+
+  public native static int GetDataLen(long mediaData);
+
+  /**
+   * @return 1完成、0未完成
+   * @brief 判断mediadata是否结束
+   */
+  public native static int IsMediaDataFinish(long mediaData);
+
+  /**
+   * 判断Windows环境
+   *
+   * @return
+   */
+  public static boolean isWindows() {
+    String osName = System.getProperties().getProperty("os.name");
+    log.info("Loading System Libraries, Current OS Version Is: {}", osName);
+    return osName.toUpperCase().contains("WINDOWS");
+  }
+
+  /**
+   * 加载系统类库
+   *
+   * @param libFiles   类库配置文件
+   * @param prefixPath 类库文件的前缀路径
+   */
+  public Finance(String[] libFiles, String prefixPath) {
+    boolean isWindows = Finance.isWindows();
+    for (String file : libFiles) {
+      String suffix = file.substring(file.lastIndexOf(".") + 1);
+      if (isWindows) {
+        // 加载dll文件
+        if (suffix.equalsIgnoreCase(DLL_FILE)) {
+          System.load(prefixPath + file);
+        }
+      } else {
+        // 加载so文件
+        if (suffix.equalsIgnoreCase(SO_FILE)) {
+          System.load(prefixPath + file);
+        }
+      }
+    }
+
+  }
+
+  /**
+   * 初始化类库文件
+   *
+   * @param libFiles
+   * @param prefixPath
+   * @return
+   */
+  public synchronized static Finance loadingLibraries(String[] libFiles, String prefixPath) {
+    if (finance != null) {
+      return finance;
+    }
+    finance = new Finance(libFiles, prefixPath);
+    return finance;
+  }
+
+  /**
+   * 单例sdk
+   *
+   * @return
+   */
+  public synchronized static long SingletonSDK() {
+    if (sdk > 0) {
+      return sdk;
+    }
+    sdk = Finance.NewSdk();
+    return sdk;
+  }
+
+  /**
+   * 销毁sdk,保证线程可见性
+   *
+   * @return
+   */
+  public synchronized static void DestroySingletonSDK(long destroySDK) {
+    sdk = 0L;
+    Finance.DestroySdk(destroySDK);
+  }
+
+}

+ 108 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java

@@ -0,0 +1,108 @@
+package me.chanjar.weixin.cp.api;
+
+import lombok.NonNull;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.msgaudit.*;
+
+import java.util.List;
+
+/**
+ * 会话内容存档接口.
+ * 官方文档:https://developer.work.weixin.qq.com/document/path/91360
+ * <p>
+ * 如需自行实现,亦可调用Finance类库函数,进行实现:
+ * com.tencent.wework.Finance
+ *
+ * @author Wang_Wong
+ * @date 2022-01-14
+ */
+public interface WxCpMsgAuditService {
+
+  /**
+   * 拉取聊天记录函数
+   *
+   * @param seq     从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+   * @param limit   一次拉取的消息条数,最大值1000条,超过1000条会返回错误
+   * @param proxy   使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+   * @param passwd  代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+   * @param timeout 超时时间,根据实际需要填写
+   * @return 返回是否调用成功
+   */
+  WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
+
+  /**
+   * 获取解密的聊天数据Model
+   *
+   * @param chatData getChatDatas()获取到的聊天数据
+   * @return 解密后的聊天数据
+   * @throws Exception
+   */
+  WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception;
+
+  /**
+   * 获取解密的聊天数据明文
+   *
+   * @param chatData getChatDatas()获取到的聊天数据
+   * @return 解密后的明文
+   * @throws Exception
+   */
+  String getChatPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception;
+
+  /**
+   * 获取媒体文件
+   * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
+   *
+   * 注意:
+   * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。
+   * 详情可以看官方文档,亦可阅读此接口源码。
+   *
+   * @param sdkfileid      消息体内容中的sdkfileid信息
+   * @param proxy          使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+   * @param passwd         代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+   * @param timeout        超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
+   * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
+   * @throws WxErrorException
+   */
+  void getMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException;
+
+  /**
+   * 获取会话内容存档开启成员列表
+   * 企业可通过此接口,获取企业开启会话内容存档的成员列表
+   * <p>
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token=ACCESS_TOKEN
+   *
+   * @param type 拉取对应版本的开启成员列表。1表示办公版;2表示服务版;3表示企业版。非必填,不填写的时候返回全量成员列表。
+   * @return
+   * @throws WxErrorException
+   */
+  List<String> getPermitUserList(Integer type) throws WxErrorException;
+
+  /**
+   * 获取会话内容存档内部群信息
+   * 企业可通过此接口,获取会话内容存档本企业的内部群信息,包括群名称、群主id、公告、群创建时间以及所有群成员的id与加入时间。
+   * <p>
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/groupchat/get?access_token=ACCESS_TOKEN
+   *
+   * @param roomid 待查询的群id
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException;
+
+  /**
+   * 获取会话同意情况
+   * 企业可通过下述接口,获取会话中外部成员的同意情况
+   * <p>
+   * 单聊请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/check_single_agree?access_token=ACCESS_TOKEN
+   * <p>
+   * 请求方式:POST(HTTPS)
+   *
+   * @param checkAgreeRequest 待查询的会话信息
+   * @return
+   * @throws WxErrorException
+   */
+  WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException;
+
+}

+ 7 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java

@@ -400,6 +400,13 @@ public interface WxCpService extends WxService {
   WxCpLivingService getLivingService();
 
   /**
+   * 获取会话存档相关接口的服务类对象
+   *
+   * @return
+   */
+  WxCpMsgAuditService getMsgAuditService();
+
+  /**
    * 获取日历相关接口的服务类对象
    *
    * @return the oa calendar service

+ 6 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java

@@ -50,6 +50,7 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
   private WxCpAgentService agentService = new WxCpAgentServiceImpl(this);
   private WxCpOaService oaService = new WxCpOaServiceImpl(this);
   private WxCpLivingService livingService = new WxCpLivingServiceImpl(this);
+  private WxCpMsgAuditService msgAuditService = new WxCpMsgAuditServiceImpl(this);
   private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this);
   private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this);
   private WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this);
@@ -485,6 +486,11 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
   }
 
   @Override
+  public WxCpMsgAuditService getMsgAuditService() {
+    return msgAuditService;
+  }
+
+  @Override
   public WxCpOaCalendarService getOaCalendarService() {
     return this.oaCalendarService;
   }

+ 196 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java

@@ -0,0 +1,196 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import com.tencent.wework.Finance;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.msgaudit.*;
+import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.List;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
+
+/**
+ * 会话内容存档接口实现类.
+ *
+ * @author Wang_Wong
+ * @date 2022-01-17
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
+  private final WxCpService cpService;
+
+  @Override
+  public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception {
+    String configPath = cpService.getWxCpConfigStorage().getMsgAuditLibPath();
+    if (StringUtils.isEmpty(configPath)) {
+      throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!");
+    }
+
+    // 替换斜杠
+    String replacePath = configPath.replace("\\", "/");
+    // 所有的后缀文件
+    String suffixFiles = replacePath.substring(replacePath.lastIndexOf("/") + 1);
+    // 获取的前缀路径
+    String prefixPath = replacePath.substring(0, replacePath.lastIndexOf("/") + 1);
+
+    // 包含so文件
+    String[] libFiles = suffixFiles.split(",");
+    if (libFiles.length <= 0) {
+      throw new WxErrorException("请仔细配置会话存档文件路径!!");
+    }
+
+    Finance.loadingLibraries(libFiles, prefixPath);
+    long sdk = Finance.SingletonSDK();
+
+    long ret = Finance.Init(sdk, cpService.getWxCpConfigStorage().getCorpId(), cpService.getWxCpConfigStorage().getCorpSecret());
+    if (ret != 0) {
+      Finance.DestroySingletonSDK(sdk);
+      throw new WxErrorException("init sdk err ret " + ret);
+    }
+
+    long slice = Finance.NewSlice();
+    ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
+    if (ret != 0) {
+      Finance.FreeSlice(slice);
+      Finance.DestroySingletonSDK(sdk);
+      throw new WxErrorException("getchatdata err ret " + ret);
+    }
+
+    // 拉取会话存档
+    String content = Finance.GetContentFromSlice(slice);
+    Finance.FreeSlice(slice);
+    WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
+    if (chatDatas.getErrCode().intValue() != 0) {
+      throw new WxErrorException(chatDatas.toJson());
+    }
+
+    return chatDatas;
+  }
+
+  @Override
+  public WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception {
+    String plainText = this.decryptChatData(chatData);
+    return WxCpChatModel.fromJson(plainText);
+  }
+
+  public String decryptChatData(WxCpChatDatas.WxCpChatData chatData) throws Exception {
+    // 企业获取的会话内容将用公钥加密,企业可用自行保存的私钥解开会话内容数据,aeskey不能为空
+    String priKey = cpService.getWxCpConfigStorage().getAesKey();
+    if (StringUtils.isEmpty(priKey)) {
+      throw new WxErrorException("请配置会话存档私钥【aesKey】");
+    }
+
+    String decryptByPriKey = WxCpCryptUtil.decryptByPriKey(chatData.getEncryptRandomKey(), priKey);
+    // 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。
+    long sdk = Finance.SingletonSDK();
+    long msg = Finance.NewSlice();
+
+    int ret = Finance.DecryptData(sdk, decryptByPriKey, chatData.getEncryptChatMsg(), msg);
+    if (ret != 0) {
+      Finance.FreeSlice(msg);
+      Finance.DestroySingletonSDK(sdk);
+      throw new WxErrorException("msg err ret " + ret);
+    }
+
+    // 明文
+    String plainText = Finance.GetContentFromSlice(msg);
+    Finance.FreeSlice(msg);
+    return plainText;
+  }
+
+  @Override
+  public String getChatPlainText(WxCpChatDatas.@NonNull WxCpChatData chatData) throws Exception {
+    return this.decryptChatData(chatData);
+  }
+
+  @Override
+  public void getMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException {
+    /**
+     * 1、媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。
+     * 2、若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。
+     * 3、indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“:表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
+     */
+    File targetFile = new File(targetFilePath);
+    if (!targetFile.getParentFile().exists()) {
+      targetFile.getParentFile().mkdirs();
+    }
+
+    String indexbuf = "";
+    int ret, data_len = 0;
+    while (true) {
+      long mediaData = Finance.NewMediaData();
+      long sdk = Finance.SingletonSDK();
+      ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData);
+      if (ret != 0) {
+        Finance.FreeMediaData(mediaData);
+        Finance.DestroySingletonSDK(sdk);
+        throw new WxErrorException("getmediadata err ret " + ret);
+      }
+
+      data_len += Finance.GetDataLen(mediaData);
+      log.info("正在分片拉取媒体文件 len:{}, data_len:{}, is_finis:{} \n", Finance.GetIndexLen(mediaData), data_len, Finance.IsMediaDataFinish(mediaData));
+
+      try {
+        // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
+        FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath), 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);
+      }
+    }
+  }
+
+  @Override
+  public List<String> getPermitUserList(Integer type) throws WxErrorException {
+    final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_PERMIT_USER_LIST);
+    JsonObject jsonObject = new JsonObject();
+    if (type != null) {
+      jsonObject.addProperty("type", type);
+    }
+    String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+    return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"),
+      new TypeToken<List<String>>() {
+      }.getType());
+  }
+
+  @Override
+  public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException {
+    final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT);
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("roomid", roomid);
+    String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+    return WxCpGroupChat.fromJson(responseContent);
+  }
+
+  @Override
+  public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException {
+    String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE);
+    String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson());
+    return WxCpAgreeInfo.fromJson(responseContent);
+  }
+
+}

+ 65 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpAgreeInfo.java

@@ -0,0 +1,65 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 获取会话同意情况返回对象.
+ *
+ * @author Wang_Wong
+ */
+@Data
+public class WxCpAgreeInfo implements Serializable {
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("errcode")
+  private Integer errcode;
+
+  @SerializedName("errmsg")
+  private String errmsg;
+
+  @SerializedName("agreeinfo")
+  private List<AgreeInfo> agreeInfo;
+
+  @Getter
+  @Setter
+  public static class AgreeInfo implements Serializable {
+    private static final long serialVersionUID = -5696099236344075582L;
+
+    @SerializedName("status_change_time")
+    private Long statusChangeTime;
+
+    @SerializedName("userid")
+    private String userid;
+
+    @SerializedName("exteranalopenid")
+    private String exteranalOpenId;
+
+    @SerializedName("agree_status")
+    private String agreeStatus;
+
+    public static AgreeInfo fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, AgreeInfo.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+  public static WxCpAgreeInfo fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpAgreeInfo.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 68 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpChatDatas.java

@@ -0,0 +1,68 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 聊天记录数据内容.
+ *
+ * @author Wang_Wong
+ */
+@Data
+public class WxCpChatDatas implements Serializable {
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("errcode")
+  private Integer errCode;
+
+  @SerializedName("errmsg")
+  private String errMsg;
+
+  @SerializedName("chatdata")
+  private List<WxCpChatData> chatData;
+
+  @Getter
+  @Setter
+  public static class WxCpChatData implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("seq")
+    private Long seq;
+
+    @SerializedName("msgid")
+    private String msgId;
+
+    @SerializedName("publickey_ver")
+    private Integer publickeyVer;
+
+    @SerializedName("encrypt_random_key")
+    private String encryptRandomKey;
+
+    @SerializedName("encrypt_chat_msg")
+    private String encryptChatMsg;
+
+    public static WxCpChatData fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, WxCpChatData.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+  public static WxCpChatDatas fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpChatDatas.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 987 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpChatModel.java

@@ -0,0 +1,987 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 聊天记录数据内容.
+ *
+ * @author Wang_Wong
+ */
+@Data
+public class WxCpChatModel implements Serializable {
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("msgid")
+  private String msgId;
+
+  @SerializedName("action")
+  private String action;
+
+  @SerializedName("send")
+  private String send;
+
+  @SerializedName("from")
+  private String from;
+
+  @SerializedName("tolist")
+  private String[] tolist;
+
+  @SerializedName("roomid")
+  private String roomId;
+
+  @SerializedName("msgtime")
+  private Long msgTime;
+
+  @SerializedName("msgtype")
+  private String msgType;
+
+  /**
+   * 文本
+   */
+  @SerializedName("text")
+  private Text text;
+
+  /**
+   * 图片
+   */
+  @SerializedName("image")
+  private Image image;
+
+  /**
+   * 撤回消息
+   */
+  @SerializedName("revoke")
+  private Revoke revoke;
+
+  /**
+   * 同意会话聊天内容
+   */
+  @SerializedName(value = "agree")
+  private Agree agree;
+
+  @SerializedName(value = "disagree")
+  private Agree disagree;
+
+  /**
+   * 语音
+   */
+  @SerializedName(value = "voice")
+  private Voice voice;
+
+  /**
+   * 视频
+   */
+  @SerializedName(value = "video")
+  private Video video;
+
+  /**
+   * 名片
+   */
+  @SerializedName(value = "card")
+  private Card card;
+
+  /**
+   * 位置
+   */
+  @SerializedName(value = "location")
+  private Location location;
+
+  /**
+   * 表情
+   */
+  @SerializedName(value = "emotion")
+  private Emotion emotion;
+
+  /**
+   * 文件
+   */
+  @SerializedName(value = "file")
+  private File file;
+
+  /**
+   * 链接
+   */
+  @SerializedName(value = "link")
+  private Link link;
+
+  /**
+   * 小程序消息
+   */
+  @SerializedName(value = "weapp")
+  private Weapp weapp;
+
+  /**
+   * 会话记录消息
+   */
+  @SerializedName(value = "chatrecord")
+  private ChatRecord chatRecord;
+
+  /**
+   * 待办消息 官网暂无
+   */
+
+  /**
+   * 投票消息 官网暂无
+   */
+
+  /**
+   * 填表消息
+   */
+  @SerializedName(value = "collect")
+  private Collect collect;
+
+  /**
+   * 红包消息
+   * 互通红包消息
+   */
+  @SerializedName("redpacket")
+  private Redpacket redPacket;
+
+  /**
+   * 会议邀请消息
+   */
+  @SerializedName("meeting")
+  private Meeting meeting;
+
+  /**
+   * 切换企业日志
+   */
+  @SerializedName("time")
+  private Long time;
+
+  @SerializedName("user")
+  private String user;
+
+  /**
+   * 在线文档消息
+   */
+  @SerializedName("doc")
+  private Doc doc;
+
+  @SerializedName("info")
+  private Info info;
+
+  /**
+   * 日程消息
+   */
+  @SerializedName("calendar")
+  private Calendar calendar;
+
+  /**
+   * 混合消息
+   */
+  @SerializedName("mixed")
+  private Mixed mixed;
+
+  /**
+   * 音频存档消息
+   */
+  @SerializedName("voiceid")
+  private String voiceId;
+
+  @SerializedName("meeting_voice_call")
+  private MeetingVoiceCall meetingVoiceCall;
+
+  /**
+   * 音频共享文档消息
+   */
+  @SerializedName("voipid")
+  private String voipId;
+
+  @SerializedName("voip_doc_share")
+  private WxCpFileItem voipDocShare;
+
+  /**
+   * 视频号消息
+   */
+  @SerializedName("sphfeed")
+  private SphFeed sphFeed;
+
+  public static WxCpChatModel fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpChatModel.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+
+  @Getter
+  @Setter
+  public static class Text implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("content")
+    private String content;
+
+    public static Text fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Text.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Image implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("md5sum")
+    private String md5Sum;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    @SerializedName("filesize")
+    private Long fileSize;
+
+    public static Image fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Image.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Revoke implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("pre_msgid")
+    private String preMsgId;
+
+    public static Revoke fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Revoke.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Agree implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("userid")
+    private String userId;
+
+    @SerializedName(value = "agree_time")
+    private Long agreeTime;
+
+    @SerializedName(value = "disagree_time")
+    private Long disagreeTime;
+
+    public static Agree fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Agree.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Voice implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("md5sum")
+    private String md5Sum;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    @SerializedName("voice_size")
+    private Long voiceSize;
+
+    @SerializedName("play_length")
+    private Long playLength;
+
+    public static Voice fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Voice.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Video implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("md5sum")
+    private String md5Sum;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    @SerializedName("filesize")
+    private Long fileSize;
+
+    @SerializedName("play_length")
+    private Long playLength;
+
+    public static Video fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Video.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Card implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("corpname")
+    private String corpName;
+
+    @SerializedName("userid")
+    private String userId;
+
+    public static Card fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Card.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Location implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("longitude")
+    private Double longitude;
+
+    @SerializedName("latitude")
+    private Double latitude;
+
+    @SerializedName("address")
+    private String address;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("zoom")
+    private Integer zoom;
+
+    public static Location fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Location.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Emotion implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("type")
+    private Integer type;
+
+    @SerializedName("width")
+    private Integer width;
+
+    @SerializedName("height")
+    private Integer height;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("imagesize")
+    private Integer imageSize;
+
+    @SerializedName("md5sum")
+    private String md5Sum;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    public static Emotion fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Emotion.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class File implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("md5sum")
+    private String md5Sum;
+
+    @SerializedName("filename")
+    private String fileName;
+
+    @SerializedName("fileext")
+    private String fileExt;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    @SerializedName("filesize")
+    private Integer fileSize;
+
+    public static File fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, File.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Link implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("description")
+    private String description;
+
+    @SerializedName("link_url")
+    private String linkUrl;
+
+    @SerializedName("image_url")
+    private String imageUrl;
+
+    public static Link fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Link.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 小程序消息
+   */
+  @Getter
+  @Setter
+  public static class Weapp implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("description")
+    private String description;
+
+    @SerializedName("username")
+    private String userName;
+
+    @SerializedName("displayname")
+    private String displayName;
+
+    public static Weapp fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Weapp.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 会话记录消息
+   */
+  @Getter
+  @Setter
+  public static class ChatRecord implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName(value = "item")
+    private List<ChatRecordItem> item;
+
+    @SerializedName("title")
+    private String title;
+
+    public static ChatRecord fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, ChatRecord.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class ChatRecordItem implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("type")
+    private String type;
+
+    @SerializedName("msgtime")
+    private Long msgTime;
+
+    @SerializedName("content")
+    private String content;
+
+    @SerializedName("from_chatroom")
+    private Boolean fromChatRoom;
+
+    public static ChatRecordItem fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, ChatRecordItem.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 填表消息
+   */
+  @Getter
+  @Setter
+  public static class Collect implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("room_name")
+    private String roomName;
+
+    @SerializedName("creator")
+    private String creator;
+
+    @SerializedName("create_time")
+    private String createTime;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("details")
+    private List<Details> details;
+
+    public static Collect fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Collect.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  @Getter
+  @Setter
+  public static class Details implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("id")
+    private Long id;
+
+    @SerializedName("ques")
+    private String ques;
+
+    @SerializedName("type")
+    private String type;
+
+    public static Details fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Details.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 红包消息
+   */
+  @Getter
+  @Setter
+  public static class Redpacket implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("type")
+    private Integer type;
+
+    @SerializedName("totalcnt")
+    private Integer totalCnt;
+
+    @SerializedName("totalamount")
+    private Integer totalAmount;
+
+    @SerializedName("wish")
+    private String wish;
+
+    public static Redpacket fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Redpacket.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 会议邀请消息
+   */
+  @Getter
+  @Setter
+  public static class Meeting implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("topic")
+    private String topic;
+
+    @SerializedName("starttime")
+    private Long startTime;
+
+    @SerializedName("endtime")
+    private Long endTime;
+
+    @SerializedName("address")
+    private String address;
+
+    @SerializedName("remarks")
+    private String remarks;
+
+    @SerializedName("meetingtype")
+    private Integer meetingType;
+
+    @SerializedName("meetingid")
+    private Long meetingId;
+
+    @SerializedName("status")
+    private Integer status;
+
+    public static Meeting fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Meeting.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 在线文档消息
+   */
+  @Getter
+  @Setter
+  public static class Doc implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("doc_creator")
+    private String docCreator;
+
+    @SerializedName("link_url")
+    private String linkUrl;
+
+    public static Doc fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Doc.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * MarkDown格式消息
+   */
+  @Getter
+  @Setter
+  public static class Info implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("content")
+    private String content;
+
+    @SerializedName("item")
+    private List<NewsItem> newsItem;
+
+    public static Info fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Info.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 图文消息
+   */
+  @Getter
+  @Setter
+  public static class NewsItem implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("description")
+    private String description;
+
+    @SerializedName("url")
+    private String url;
+
+    @SerializedName("picurl")
+    private String picUrl;
+
+    public static NewsItem fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, NewsItem.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 日程消息
+   */
+  @Getter
+  @Setter
+  public static class Calendar implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("title")
+    private String title;
+
+    @SerializedName("creatorname")
+    private String creatorName;
+
+    @SerializedName("attendeename")
+    private String[] attendeeName;
+
+    @SerializedName("starttime")
+    private Long startTime;
+
+    @SerializedName("endtime")
+    private Long endTime;
+
+    @SerializedName("place")
+    private String place;
+
+    @SerializedName("remarks")
+    private String remarks;
+
+    public static Calendar fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Calendar.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+  /**
+   * 混合消息
+   */
+  @Getter
+  @Setter
+  public static class Mixed implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("item")
+    private List<Item> item;
+
+    @Getter
+    @Setter
+    public static class Item implements Serializable {
+      private static final long serialVersionUID = -5028321625140879571L;
+
+      @SerializedName("type")
+      private String type;
+
+      @SerializedName("content")
+      private String content;
+
+    }
+
+  }
+
+
+  /**
+   * 音频存档消息
+   */
+  @Getter
+  @Setter
+  public static class MeetingVoiceCall implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("endtime")
+    private Long endTime;
+
+    @SerializedName("sdkfileid")
+    private String sdkFileId;
+
+    @SerializedName("demofiledata")
+    private List<DemoFileData> demoFileData;
+
+    @SerializedName("sharescreendata")
+    private List<ShareScreenData> shareScreenData;
+
+    public static MeetingVoiceCall fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, MeetingVoiceCall.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+    @Getter
+    @Setter
+    public static class DemoFileData implements Serializable {
+      private static final long serialVersionUID = -5028321625140879571L;
+
+      @SerializedName("filename")
+      private String fileName;
+
+      @SerializedName("demooperator")
+      private String demoOperator;
+
+      @SerializedName("starttime")
+      private Long startTime;
+
+      @SerializedName("endtime")
+      private Long endTime;
+
+      public static DemoFileData fromJson(String json) {
+        return WxCpGsonBuilder.create().fromJson(json, DemoFileData.class);
+      }
+
+      public String toJson() {
+        return WxCpGsonBuilder.create().toJson(this);
+      }
+
+    }
+
+    @Getter
+    @Setter
+    public static class ShareScreenData implements Serializable {
+      private static final long serialVersionUID = -5028321625140879571L;
+
+      @SerializedName("share")
+      private String share;
+
+      @SerializedName("starttime")
+      private Long startTime;
+
+      @SerializedName("endtime")
+      private Long endTime;
+
+      public static ShareScreenData fromJson(String json) {
+        return WxCpGsonBuilder.create().fromJson(json, ShareScreenData.class);
+      }
+
+      public String toJson() {
+        return WxCpGsonBuilder.create().toJson(this);
+      }
+
+    }
+
+  }
+
+
+  /**
+   * 视频号消息
+   */
+  @Getter
+  @Setter
+  public static class SphFeed implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("feed_type")
+    private Integer feedType;
+
+    @SerializedName("sph_name")
+    private String sphName;
+
+    @SerializedName("feed_desc")
+    private String feedDesc;
+
+    public static SphFeed fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, SphFeed.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+
+}

+ 57 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpCheckAgreeRequest.java

@@ -0,0 +1,57 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.*;
+import lombok.experimental.Accessors;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 获取会话同意情况请求参数.
+ *
+ * @author Wang_Wong
+ * @date 2022-01-21
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class WxCpCheckAgreeRequest implements Serializable {
+  private static final long serialVersionUID = -4960239393895754138L;
+
+  @SerializedName("info")
+  private List<Info> info;
+
+  public static WxCpCheckAgreeRequest fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpCheckAgreeRequest.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+  @Getter
+  @Setter
+  public static class Info implements Serializable {
+    private static final long serialVersionUID = -4960239393895754138L;
+
+    @SerializedName("userid")
+    private String userid;
+
+    @SerializedName("exteranalopenid")
+    private String exteranalOpenId;
+
+    public static Info fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Info.class);
+    }
+
+    public String toJson() {
+      return WxCpGsonBuilder.create().toJson(this);
+    }
+
+  }
+
+}

+ 39 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpFileItem.java

@@ -0,0 +1,39 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 会话存档 文档信息对象
+ *
+ * @author Wang_Wong
+ */
+@Data
+public class WxCpFileItem implements Serializable {
+
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("filename")
+  private String fileName;
+
+  @SerializedName("md5sum")
+  private String md5Sum;
+
+  @SerializedName("sdkfileid")
+  private String sdkFileId;
+
+  @SerializedName("filesize")
+  private Long fileSize;
+
+  public static WxCpFileItem fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpFileItem.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 61 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/msgaudit/WxCpGroupChat.java

@@ -0,0 +1,61 @@
+package me.chanjar.weixin.cp.bean.msgaudit;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 内部群信息
+ *
+ * @author Wang_Wong
+ */
+@Data
+public class WxCpGroupChat implements Serializable {
+
+  private static final long serialVersionUID = -5028321625140879571L;
+
+  @SerializedName("roomname")
+  private String roomName;
+
+  @SerializedName("creator")
+  private String creator;
+
+  @SerializedName("room_create_time")
+  private Long roomCreateTime;
+
+  @SerializedName("notice")
+  private String notice;
+
+  private List<Member> members;
+
+  @Getter
+  @Setter
+  public class Member implements Serializable {
+    private static final long serialVersionUID = -5028321625140879571L;
+
+    @SerializedName("memberid")
+    private String memberId;
+
+    @SerializedName("jointime")
+    private Long joinTime;
+
+    public Member fromJson(String json) {
+      return WxCpGsonBuilder.create().fromJson(json, Member.class);
+    }
+
+  }
+
+  public static WxCpGroupChat fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpGroupChat.class);
+  }
+
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}

+ 7 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java

@@ -175,6 +175,13 @@ public interface WxCpConfigStorage {
   String getAesKey();
 
   /**
+   * 获取企微会话存档系统库 绝对路径
+   *
+   * @return
+   */
+  String getMsgAuditLibPath();
+
+  /**
    * Gets expires time.
    *
    * @return the expires time

+ 6 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java

@@ -43,6 +43,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   private volatile String token;
   private volatile String aesKey;
   private volatile long expiresTime;
+  private volatile String msgAuditLibPath;
   private volatile String oauth2redirectUri;
   private volatile String httpProxyHost;
   private volatile int httpProxyPort;
@@ -256,6 +257,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
     return this.aesKey;
   }
 
+  @Override
+  public String getMsgAuditLibPath() {
+    return this.msgAuditLibPath;
+  }
+
   /**
    * Sets aes key.
    *

+ 6 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java

@@ -40,6 +40,7 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   private volatile String token;
   private volatile String aesKey;
   private volatile Integer agentId;
+  private volatile String msgAuditLibPath;
   private volatile String oauth2redirectUri;
   private volatile String httpProxyHost;
   private volatile int httpProxyPort;
@@ -320,6 +321,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
     return this.aesKey;
   }
 
+  @Override
+  public String getMsgAuditLibPath() {
+    return this.msgAuditLibPath;
+  }
+
   /**
    * Sets aes key.
    *

+ 6 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java

@@ -129,6 +129,12 @@ public interface WxCpApiPathConsts {
     String DELETE_REPLAY_DATA = "/cgi-bin/living/delete_replay_data";
   }
 
+  interface MsgAudit {
+    String GET_PERMIT_USER_LIST = "/cgi-bin/msgaudit/get_permit_user_list";
+    String GET_GROUP_CHAT = "/cgi-bin/msgaudit/groupchat/get";
+    String CHECK_SINGLE_AGREE = "/cgi-bin/msgaudit/check_single_agree";
+  }
+
   interface Tag {
     String TAG_CREATE = "/cgi-bin/tag/create";
     String TAG_UPDATE = "/cgi-bin/tag/update";

+ 33 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpCryptUtil.java

@@ -4,6 +4,12 @@ import com.google.common.base.CharMatcher;
 import com.google.common.io.BaseEncoding;
 import me.chanjar.weixin.common.util.crypto.WxCryptUtil;
 import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
 
 public class WxCpCryptUtil extends WxCryptUtil {
   public WxCpCryptUtil(WxCpConfigStorage wxCpConfigStorage) {
@@ -21,4 +27,31 @@ public class WxCpCryptUtil extends WxCryptUtil {
     this.aesKey = BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(encodingAesKey));
   }
 
+  /**
+   * 会话存档接口解密私钥
+   * 企业获取的会话内容将用公钥加密,企业用自行保存的私钥解开会话内容数据
+   *
+   * @param encryptRandomKey
+   * @param msgAuditPriKey
+   * @return
+   * @throws Exception
+   */
+  public static String decryptByPriKey(String encryptRandomKey, String msgAuditPriKey) throws Exception {
+    String privateKey = msgAuditPriKey.replaceAll("\\n", "")
+      .replace("-----BEGIN PRIVATE KEY-----", "")
+      .replace("-----END PRIVATE KEY-----", "")
+      .replaceAll(" ", "");
+
+    byte[] keyByte = Base64.decodeBase64(privateKey);
+    PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyByte);
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+    PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);
+
+    Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+    cipher.init(Cipher.DECRYPT_MODE, priKey);
+    byte[] utf8 = cipher.doFinal(Base64.decodeBase64(encryptRandomKey));
+
+    return new String(utf8, "UTF-8");
+  }
+
 }

File diff suppressed because it is too large
+ 433 - 0
weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java