From 990eb71703adbc3c713297361c9cae93a3fd4d47 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:26:17 +0800 Subject: [PATCH] =?UTF-8?q?:new:=20#3931=E3=80=90=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E3=80=91=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cp/tp/service/WxCpTpMessageService.java | 86 +++++++++++++ .../weixin/cp/tp/service/WxCpTpService.java | 18 +++ .../service/impl/BaseWxCpTpServiceImpl.java | 11 ++ .../impl/WxCpTpMessageServiceImpl.java | 66 ++++++++++ .../impl/WxCpTpMessageServiceImplTest.java | 118 ++++++++++++++++++ weixin-java-cp/src/test/resources/testng.xml | 1 + 6 files changed, 300 insertions(+) create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java new file mode 100644 index 000000000..edd1a96c8 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java @@ -0,0 +1,86 @@ +package me.chanjar.weixin.cp.tp.service; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.message.*; + +/** + * 企业微信第三方应用消息推送接口. + * + *

第三方应用使用授权企业的 access_token 代表授权企业发送应用消息。

+ * + * @author GitHub Copilot + */ +public interface WxCpTpMessageService { + + /** + *
+   * 发送应用消息(代授权企业发送).
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
+   * 
+ * + * @param message 要发送的消息对象 + * @param corpId 授权企业的 corpId + * @return 消息发送结果 + * @throws WxErrorException 微信错误异常 + */ + WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException; + + /** + *
+   * 查询应用消息发送统计.
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
+   * 
+ * + * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。 + * @param corpId 授权企业的 corpId + * @return 统计结果 + * @throws WxErrorException 微信错误异常 + */ + WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException; + + /** + *
+   * 互联企业发送应用消息.
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
+   * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
+   * 
+ * + * @param message 要发送的消息对象 + * @param corpId 授权企业的 corpId + * @return 消息发送结果 + * @throws WxErrorException 微信错误异常 + */ + WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) throws WxErrorException; + + /** + *
+   * 发送「学校通知」.
+   * https://developer.work.weixin.qq.com/document/path/92321
+   * 学校可以通过此接口来给家长发送不同类型的学校通知。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/message/send?access_token=ACCESS_TOKEN
+   * 
+ * + * @param message 要发送的消息对象 + * @param corpId 授权企业的 corpId + * @return 消息发送结果 + * @throws WxErrorException 微信错误异常 + */ + WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) throws WxErrorException; + + /** + *
+   * 撤回应用消息.
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=ACCESS_TOKEN
+   * 文档地址: https://developer.work.weixin.qq.com/document/path/94867
+   * 
+ * + * @param msgId 消息id + * @param corpId 授权企业的 corpId + * @throws WxErrorException 微信错误异常 + */ + void recall(String msgId, String corpId) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java index 92966c1d0..5189c5d82 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java @@ -530,6 +530,24 @@ public interface WxCpTpService { */ WxCpTpLicenseService getWxCpTpLicenseService(); + /** + * get message service + * + * @return WxCpTpMessageService wx cp tp message service + */ + default WxCpTpMessageService getWxCpTpMessageService() { + throw new UnsupportedOperationException("WxCpTpMessageService is not supported"); + } + + /** + * set message service + * + * @param wxCpTpMessageService the message service + */ + default void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) { + throw new UnsupportedOperationException("WxCpTpMessageService is not supported"); + } + WxCpTpXmlMessage fromEncryptedXml(String encryptedXml, String timestamp, String nonce, String msgSignature); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java index 25c1470eb..d8ed7c0e4 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java @@ -61,6 +61,7 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ private WxCpTpIdConvertService wxCpTpIdConvertService = new WxCpTpIdConvertServiceImpl(this); private WxCpTpOAuth2Service wxCpTpOAuth2Service = new WxCpTpOAuth2ServiceImpl(this); private WxCpTpCustomizedService wxCpTpCustomizedService = new WxCpTpCustomizedServiceImpl(this); + private WxCpTpMessageService wxCpTpMessageService = new WxCpTpMessageServiceImpl(this); /** * 全局的是否正在刷新access token的锁. */ @@ -665,6 +666,16 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ this.wxCpTpLicenseService = wxCpTpLicenseService; } + @Override + public WxCpTpMessageService getWxCpTpMessageService() { + return wxCpTpMessageService; + } + + @Override + public void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) { + this.wxCpTpMessageService = wxCpTpMessageService; + } + @Override public void setWxCpTpUserService(WxCpTpUserService wxCpTpUserService) { this.wxCpTpUserService = wxCpTpUserService; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java new file mode 100644 index 000000000..ef80d01ac --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java @@ -0,0 +1,66 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.message.*; +import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.*; + +/** + * 企业微信第三方应用消息推送接口实现类. + * + *

代授权企业发送应用消息,所有方法均需传入授权企业的 corpId。

+ * + * @author GitHub Copilot + */ +@RequiredArgsConstructor +public class WxCpTpMessageServiceImpl implements WxCpTpMessageService { + + private final WxCpTpService mainService; + + @Override + public WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_SEND) + + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId); + return WxCpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true)); + } + + @Override + public WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(GET_STATISTICS) + + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId); + return WxCpMessageSendStatistics.fromJson( + this.mainService.post(url, WxCpGsonBuilder.create().toJson(ImmutableMap.of("time_type", timeType)), true)); + } + + @Override + public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) + throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(LINKEDCORP_MESSAGE_SEND) + + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId); + return WxCpLinkedCorpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true)); + } + + @Override + public WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) + throws WxErrorException { + String url = mainService.getWxCpTpConfigStorage().getApiUrl(EXTERNAL_CONTACT_MESSAGE_SEND) + + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId); + return WxCpSchoolContactMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true)); + } + + @Override + public void recall(String msgId, String corpId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("msgid", msgId); + String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_RECALL) + + "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId); + this.mainService.post(url, jsonObject.toString(), true); + } + +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java new file mode 100644 index 000000000..ff0a143b7 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImplTest.java @@ -0,0 +1,118 @@ +package me.chanjar.weixin.cp.tp.service.impl; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.message.WxCpMessage; +import me.chanjar.weixin.cp.bean.message.WxCpMessageSendResult; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_RECALL; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_SEND; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertNotNull; + +/** + * 企业微信第三方应用消息推送服务测试. + * + * @author GitHub Copilot + */ +public class WxCpTpMessageServiceImplTest { + + @Mock + private WxCpTpServiceApacheHttpClientImpl wxCpTpService; + + @Mock + private WxCpTpConfigStorage configStorage; + + private WxCpTpMessageService wxCpTpMessageService; + + private AutoCloseable mockitoAnnotations; + + /** + * Sets up. + */ + @BeforeClass + public void setUp() { + mockitoAnnotations = MockitoAnnotations.openMocks(this); + when(wxCpTpService.getWxCpTpConfigStorage()).thenReturn(configStorage); + WxCpTpDefaultConfigImpl defaultConfig = new WxCpTpDefaultConfigImpl(); + when(configStorage.getApiUrl(anyString())) + .thenAnswer(invocation -> defaultConfig.getApiUrl(invocation.getArgument(0))); + wxCpTpMessageService = new WxCpTpMessageServiceImpl(wxCpTpService); + } + + /** + * Tear down. + * + * @throws Exception the exception + */ + @AfterClass + public void tearDown() throws Exception { + mockitoAnnotations.close(); + } + + /** + * 测试 send 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求. + * + * @throws WxErrorException 微信错误异常 + */ + @Test + public void testSendMessage() throws WxErrorException { + String corpId = "test_corp_id"; + String accessToken = "test_access_token"; + String mockResponse = "{\"errcode\":0,\"errmsg\":\"ok\",\"msgid\":\"msg_001\"}"; + + when(configStorage.getAccessToken(corpId)).thenReturn(accessToken); + String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_SEND) + + "?access_token=" + accessToken; + when(wxCpTpService.post(eq(expectedUrl), anyString(), eq(true))).thenReturn(mockResponse); + + WxCpMessage message = WxCpMessage.TEXT().toUser("zhangsan").content("hello").agentId(1).build(); + WxCpMessageSendResult result = wxCpTpMessageService.send(message, corpId); + assertNotNull(result); + + // 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token + verify(wxCpTpService).post(eq(expectedUrl), anyString(), eq(true)); + } + + /** + * 测试 recall 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求. + * + * @throws WxErrorException 微信错误异常 + */ + @Test + public void testRecallMessage() throws WxErrorException { + String corpId = "test_corp_id"; + String accessToken = "test_access_token"; + String msgId = "test_msg_id"; + + when(configStorage.getAccessToken(corpId)).thenReturn(accessToken); + String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_RECALL) + + "?access_token=" + accessToken; + when(wxCpTpService.post(eq(expectedUrl), contains(msgId), eq(true))).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}"); + + wxCpTpMessageService.recall(msgId, corpId); + + // 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token + verify(wxCpTpService).post(eq(expectedUrl), contains(msgId), eq(true)); + } + + /** + * 测试 getWxCpTpMessageService 方法:验证 BaseWxCpTpServiceImpl 中正确初始化了消息服务. + */ + @Test + public void testGetWxCpTpMessageServiceFromBase() { + WxCpTpServiceApacheHttpClientImpl tpService = new WxCpTpServiceApacheHttpClientImpl(); + assertNotNull(tpService.getWxCpTpMessageService()); + } +} diff --git a/weixin-java-cp/src/test/resources/testng.xml b/weixin-java-cp/src/test/resources/testng.xml index 48589e709..6508fc220 100644 --- a/weixin-java-cp/src/test/resources/testng.xml +++ b/weixin-java-cp/src/test/resources/testng.xml @@ -9,6 +9,7 @@ +