From 54be19d4e3350ede211f4ec9fd1a5cc8fd1371d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:55:07 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0SDK=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=9C=BA=E5=88=B6=E5=92=8C=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=AE=89=E5=85=A8API=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../weixin/cp/api/WxCpMsgAuditService.java | 86 +++++++++++ .../cp/api/impl/WxCpMsgAuditServiceImpl.java | 135 ++++++++++++++++++ .../weixin/cp/config/WxCpConfigStorage.java | 26 ++++ .../cp/config/impl/WxCpDefaultConfigImpl.java | 30 ++++ .../cp/config/impl/WxCpRedisConfigImpl.java | 30 ++++ 5 files changed, 307 insertions(+) diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java index 221caf2e7..b754e32b7 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java @@ -28,9 +28,26 @@ public interface WxCpMsgAuditService { * @param timeout 超时时间,根据实际需要填写 * @return 返回是否调用成功 chat datas * @throws Exception the exception + * @deprecated 请使用 {@link #getChatRecords(long, long, String, String, long)} 代替, + * 该方法会将SDK暴露给调用方,容易导致SDK生命周期管理混乱,引发JVM崩溃 */ + @Deprecated WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception; + /** + * 拉取聊天记录函数(推荐使用) + * 该方法不会将SDK暴露给调用方,SDK生命周期由框架自动管理,更加安全 + * + * @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 返回聊天记录列表,不包含SDK信息 + * @throws Exception the exception + */ + List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception; + /** * 获取解密的聊天数据Model * @@ -39,10 +56,24 @@ public interface WxCpMsgAuditService { * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... * @return 解密后的聊天数据 decrypt data * @throws Exception the exception + * @deprecated 请使用 {@link #getDecryptChatData(WxCpChatDatas.WxCpChatData, Integer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 */ + @Deprecated WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + /** + * 获取解密的聊天数据Model(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * + * @param chatData 聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的聊天数据 + * @throws Exception the exception + */ + WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + /** * 获取解密的聊天数据明文 * @@ -51,9 +82,23 @@ public interface WxCpMsgAuditService { * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... * @return 解密后的明文 chat plain text * @throws Exception the exception + * @deprecated 请使用 {@link #getChatRecordPlainText(WxCpChatDatas.WxCpChatData, Integer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 */ + @Deprecated String getChatPlainText(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + /** + * 获取解密的聊天数据明文(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * + * @param chatData 聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的明文 + * @throws Exception the exception + */ + String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + /** * 获取媒体文件 * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 @@ -69,10 +114,32 @@ public interface WxCpMsgAuditService { * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif * @throws WxErrorException the wx error exception + * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, String)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 */ + @Deprecated void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException; + /** + * 获取媒体文件(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * 针对图片、文件等媒体数据,提供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 the wx error exception + */ + void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull String targetFilePath) throws WxErrorException; + /** * 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活 * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 @@ -85,10 +152,29 @@ public interface WxCpMsgAuditService { * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 * @param action 传入一个lambda,each所有的数据分片 * @throws WxErrorException the wx error exception + * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, Consumer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 */ + @Deprecated void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull Consumer action) throws WxErrorException; + /** + * 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * 针对图片、文件等媒体数据,提供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 action 传入一个lambda,each所有的数据分片 + * @throws WxErrorException the wx error exception + */ + void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException; + /** * 获取会话内容存档开启成员列表 * 企业可通过此接口,获取企业开启会话内容存档的成员列表 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java index cdf559ad7..5bfecd13b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java @@ -280,4 +280,139 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService { return WxCpAgreeInfo.fromJson(responseContent); } + @Override + public List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, + @NonNull long timeout) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + long slice = Finance.NewSlice(); + long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice); + if (ret != 0) { + Finance.FreeSlice(slice); + 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.getChatData(); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + String plainText = this.decryptChatData(sdk, chatData, pkcs1); + return WxCpChatModel.fromJson(plainText); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + // 获取或初始化SDK + long sdk = this.initSdk(); + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + return this.decryptChatData(sdk, chatData, pkcs1); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull String targetFilePath) throws WxErrorException { + // 获取或初始化SDK + long sdk; + try { + sdk = this.initSdk(); + } catch (WxErrorException e) { + throw e; + } catch (Exception e) { + throw new WxErrorException(e); + } + + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + File targetFile = new File(targetFilePath); + if (!targetFile.getParentFile().exists()) { + targetFile.getParentFile().mkdirs(); + } + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> { + try { + // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + FileOutputStream outputStream = new FileOutputStream(targetFile, true); + outputStream.write(i); + outputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + + @Override + public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException { + // 获取或初始化SDK + long sdk; + try { + sdk = this.initSdk(); + } catch (WxErrorException e) { + throw e; + } catch (Exception e) { + throw new WxErrorException(e); + } + + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + // 增加引用计数 + configStorage.incrementMsgAuditSdkRefCount(sdk); + + try { + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action); + } finally { + // 减少引用计数 + configStorage.decrementMsgAuditSdkRefCount(sdk); + } + } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index 8b968e540..f9c0e5d94 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -292,4 +292,30 @@ public interface WxCpConfigStorage { * 使会话存档SDK过期 */ void expireMsgAuditSdk(); + + /** + * 增加会话存档SDK的引用计数 + * 用于支持多线程安全的SDK生命周期管理 + * + * @param sdk sdk id + * @return 增加后的引用计数 + */ + int incrementMsgAuditSdkRefCount(long sdk); + + /** + * 减少会话存档SDK的引用计数 + * 当引用计数降为0时,自动销毁SDK + * + * @param sdk sdk id + * @return 减少后的引用计数,如果返回0表示SDK已被销毁 + */ + int decrementMsgAuditSdkRefCount(long sdk); + + /** + * 获取会话存档SDK的引用计数 + * + * @param sdk sdk id + * @return 当前引用计数 + */ + int getMsgAuditSdkRefCount(long sdk); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index 4bf13f24e..cd2bdb45d 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -54,6 +54,10 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { */ private volatile long msgAuditSdk; private volatile long msgAuditSdkExpiresTime; + /** + * 会话存档SDK引用计数,用于多线程安全的生命周期管理 + */ + private volatile int msgAuditSdkRefCount; private volatile String oauth2redirectUri; private volatile String httpProxyHost; private volatile int httpProxyPort; @@ -473,10 +477,36 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + // 重置引用计数 + this.msgAuditSdkRefCount = 0; } @Override public void expireMsgAuditSdk() { this.msgAuditSdkExpiresTime = 0; } + + @Override + public synchronized int incrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return ++this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int decrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { + return --this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int getMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return this.msgAuditSdkRefCount; + } + return 0; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 49cd7c455..93ec3d567 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -55,6 +55,10 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { */ private volatile long msgAuditSdk; private volatile long msgAuditSdkExpiresTime; + /** + * 会话存档SDK引用计数,用于多线程安全的生命周期管理 + */ + private volatile int msgAuditSdkRefCount; /** * Instantiates a new Wx cp redis config. @@ -491,10 +495,36 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { this.msgAuditSdk = sdk; // 预留200秒的时间 this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + // 重置引用计数 + this.msgAuditSdkRefCount = 0; } @Override public void expireMsgAuditSdk() { this.msgAuditSdkExpiresTime = 0; } + + @Override + public synchronized int incrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return ++this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int decrementMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk && this.msgAuditSdkRefCount > 0) { + return --this.msgAuditSdkRefCount; + } + return 0; + } + + @Override + public synchronized int getMsgAuditSdkRefCount(long sdk) { + if (this.msgAuditSdk == sdk) { + return this.msgAuditSdkRefCount; + } + return 0; + } }