1
0
mirror of synced 2026-04-13 12:44:58 +08:00

feat: 为企业微信第三方应用SDK添加消息发送服务(WxCpTpMessageService)

Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-17 02:38:37 +00:00
parent 4c6562340f
commit 1f9f82c10b
6 changed files with 296 additions and 0 deletions

View File

@@ -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>
* 查询应用消息发送统计.
* 请求方式POSTHTTPS
* 请求地址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
* 学校可以通过此接口来给家长发送不同类型的学校通知。
* 请求方式POSTHTTPS
* 请求地址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;
}

View File

@@ -530,6 +530,20 @@ public interface WxCpTpService {
*/
WxCpTpLicenseService getWxCpTpLicenseService();
/**
* get message service
*
* @return WxCpTpMessageService wx cp tp message service
*/
WxCpTpMessageService getWxCpTpMessageService();
/**
* set message service
*
* @param wxCpTpMessageService the message service
*/
void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService);
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature);

View File

@@ -61,6 +61,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> 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<H, P> 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;

View File

@@ -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)
+ "?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);
}
}

View File

@@ -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());
}
}

View File

@@ -9,6 +9,7 @@
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImplTest"/>
<class name="me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImplTest"/>
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpTagServiceImplTest"/>
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpMessageServiceImplTest"/>
</classes>
</test>