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 0000000000..edd1a96c85 --- /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 92966c1d03..5189c5d821 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 25c1470eb2..d8ed7c0e47 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 void setWxCpTpLicenseService(WxCpTpLicenseService wxCpTpLicenseService) { 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 0000000000..ef80d01ac1 --- /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 0000000000..ff0a143b71 --- /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 81f5a42fcb..c26226083c 100644 --- a/weixin-java-cp/src/test/resources/testng.xml +++ b/weixin-java-cp/src/test/resources/testng.xml @@ -9,6 +9,7 @@ +