-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
feat(cp-tp): Add WxCpTpMessageService for third-party app message sending #3931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.*; | ||
|
|
||
| /** | ||
| * 企业微信第三方应用消息推送接口. | ||
| * | ||
| * <p>第三方应用使用授权企业的 access_token 代表授权企业发送应用消息。</p> | ||
| * | ||
| * @author <a href="https://github.com/github-copilot">GitHub Copilot</a> | ||
| */ | ||
| public interface WxCpTpMessageService { | ||
|
|
||
| /** | ||
| * <pre> | ||
| * 发送应用消息(代授权企业发送). | ||
| * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236 | ||
| * </pre> | ||
| * | ||
| * @param message 要发送的消息对象 | ||
| * @param corpId 授权企业的 corpId | ||
| * @return 消息发送结果 | ||
| * @throws WxErrorException 微信错误异常 | ||
| */ | ||
| WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException; | ||
|
|
||
| /** | ||
| * <pre> | ||
| * 查询应用消息发送统计. | ||
| * 请求方式: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 | ||
| * </pre> | ||
| * | ||
| * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。 | ||
| * @param corpId 授权企业的 corpId | ||
| * @return 统计结果 | ||
| * @throws WxErrorException 微信错误异常 | ||
| */ | ||
| WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException; | ||
|
|
||
| /** | ||
| * <pre> | ||
| * 互联企业发送应用消息. | ||
| * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN | ||
| * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250 | ||
| * </pre> | ||
| * | ||
| * @param message 要发送的消息对象 | ||
| * @param corpId 授权企业的 corpId | ||
| * @return 消息发送结果 | ||
| * @throws WxErrorException 微信错误异常 | ||
| */ | ||
| WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) throws WxErrorException; | ||
|
|
||
| /** | ||
| * <pre> | ||
| * 发送「学校通知」. | ||
| * 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 | ||
| * </pre> | ||
| * | ||
| * @param message 要发送的消息对象 | ||
| * @param corpId 授权企业的 corpId | ||
| * @return 消息发送结果 | ||
| * @throws WxErrorException 微信错误异常 | ||
| */ | ||
| WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) throws WxErrorException; | ||
|
|
||
| /** | ||
| * <pre> | ||
| * 撤回应用消息. | ||
| * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=ACCESS_TOKEN | ||
| * 文档地址: https://developer.work.weixin.qq.com/document/path/94867 | ||
| * </pre> | ||
| * | ||
| * @param msgId 消息id | ||
| * @param corpId 授权企业的 corpId | ||
| * @throws WxErrorException 微信错误异常 | ||
| */ | ||
| void recall(String msgId, String corpId) throws WxErrorException; | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.*; | ||
|
|
||
| /** | ||
| * 企业微信第三方应用消息推送接口实现类. | ||
| * | ||
| * <p>代授权企业发送应用消息,所有方法均需传入授权企业的 corpId。</p> | ||
| * | ||
| * @author <a href="https://github.com/github-copilot">GitHub Copilot</a> | ||
| */ | ||
| @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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里用 Severity: low Other Locations
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| + "?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); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TP 场景下如果消息体未显式设置
agentId(例如WxCpMessage/WxCpLinkedCorpMessage/WxCpSchoolContactMessage),接口通常会直接返回参数错误;这里建议在接口 Javadoc 里明确“调用方必须自行设置 agentId”,避免误用。Severity: low
Other Locations
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java:55weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java:71🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.