Skip to content

Commit 1f9f82c

Browse files
Copilotbinarywang
andcommitted
feat: 为企业微信第三方应用SDK添加消息发送服务(WxCpTpMessageService)
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
1 parent 4c65623 commit 1f9f82c

6 files changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package me.chanjar.weixin.cp.tp.service;
2+
3+
import me.chanjar.weixin.common.error.WxErrorException;
4+
import me.chanjar.weixin.cp.bean.message.*;
5+
6+
/**
7+
* 企业微信第三方应用消息推送接口.
8+
*
9+
* <p>第三方应用使用授权企业的 access_token 代表授权企业发送应用消息。</p>
10+
*
11+
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
12+
*/
13+
public interface WxCpTpMessageService {
14+
15+
/**
16+
* <pre>
17+
* 发送应用消息(代授权企业发送).
18+
* 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
19+
* </pre>
20+
*
21+
* @param message 要发送的消息对象
22+
* @param corpId 授权企业的 corpId
23+
* @return 消息发送结果
24+
* @throws WxErrorException 微信错误异常
25+
*/
26+
WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException;
27+
28+
/**
29+
* <pre>
30+
* 查询应用消息发送统计.
31+
* 请求方式:POST(HTTPS)
32+
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
33+
* 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
34+
* </pre>
35+
*
36+
* @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。
37+
* @param corpId 授权企业的 corpId
38+
* @return 统计结果
39+
* @throws WxErrorException 微信错误异常
40+
*/
41+
WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException;
42+
43+
/**
44+
* <pre>
45+
* 互联企业发送应用消息.
46+
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
47+
* 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
48+
* </pre>
49+
*
50+
* @param message 要发送的消息对象
51+
* @param corpId 授权企业的 corpId
52+
* @return 消息发送结果
53+
* @throws WxErrorException 微信错误异常
54+
*/
55+
WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) throws WxErrorException;
56+
57+
/**
58+
* <pre>
59+
* 发送「学校通知」.
60+
* https://developer.work.weixin.qq.com/document/path/92321
61+
* 学校可以通过此接口来给家长发送不同类型的学校通知。
62+
* 请求方式:POST(HTTPS)
63+
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/message/send?access_token=ACCESS_TOKEN
64+
* </pre>
65+
*
66+
* @param message 要发送的消息对象
67+
* @param corpId 授权企业的 corpId
68+
* @return 消息发送结果
69+
* @throws WxErrorException 微信错误异常
70+
*/
71+
WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) throws WxErrorException;
72+
73+
/**
74+
* <pre>
75+
* 撤回应用消息.
76+
* 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=ACCESS_TOKEN
77+
* 文档地址: https://developer.work.weixin.qq.com/document/path/94867
78+
* </pre>
79+
*
80+
* @param msgId 消息id
81+
* @param corpId 授权企业的 corpId
82+
* @throws WxErrorException 微信错误异常
83+
*/
84+
void recall(String msgId, String corpId) throws WxErrorException;
85+
86+
}

weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,20 @@ public interface WxCpTpService {
530530
*/
531531
WxCpTpLicenseService getWxCpTpLicenseService();
532532

533+
/**
534+
* get message service
535+
*
536+
* @return WxCpTpMessageService wx cp tp message service
537+
*/
538+
WxCpTpMessageService getWxCpTpMessageService();
539+
540+
/**
541+
* set message service
542+
*
543+
* @param wxCpTpMessageService the message service
544+
*/
545+
void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService);
546+
533547
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
534548
String timestamp, String nonce, String msgSignature);
535549

weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
6161
private WxCpTpIdConvertService wxCpTpIdConvertService = new WxCpTpIdConvertServiceImpl(this);
6262
private WxCpTpOAuth2Service wxCpTpOAuth2Service = new WxCpTpOAuth2ServiceImpl(this);
6363
private WxCpTpCustomizedService wxCpTpCustomizedService = new WxCpTpCustomizedServiceImpl(this);
64+
private WxCpTpMessageService wxCpTpMessageService = new WxCpTpMessageServiceImpl(this);
6465
/**
6566
* 全局的是否正在刷新access token的锁.
6667
*/
@@ -665,6 +666,16 @@ public void setWxCpTpLicenseService(WxCpTpLicenseService wxCpTpLicenseService) {
665666
this.wxCpTpLicenseService = wxCpTpLicenseService;
666667
}
667668

669+
@Override
670+
public WxCpTpMessageService getWxCpTpMessageService() {
671+
return wxCpTpMessageService;
672+
}
673+
674+
@Override
675+
public void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) {
676+
this.wxCpTpMessageService = wxCpTpMessageService;
677+
}
678+
668679
@Override
669680
public void setWxCpTpUserService(WxCpTpUserService wxCpTpUserService) {
670681
this.wxCpTpUserService = wxCpTpUserService;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package me.chanjar.weixin.cp.tp.service.impl;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import com.google.gson.JsonObject;
5+
import lombok.RequiredArgsConstructor;
6+
import me.chanjar.weixin.common.error.WxErrorException;
7+
import me.chanjar.weixin.cp.bean.message.*;
8+
import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService;
9+
import me.chanjar.weixin.cp.tp.service.WxCpTpService;
10+
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
11+
12+
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.*;
13+
14+
/**
15+
* 企业微信第三方应用消息推送接口实现类.
16+
*
17+
* <p>代授权企业发送应用消息,所有方法均需传入授权企业的 corpId。</p>
18+
*
19+
* @author <a href="https://github.com/github-copilot">GitHub Copilot</a>
20+
*/
21+
@RequiredArgsConstructor
22+
public class WxCpTpMessageServiceImpl implements WxCpTpMessageService {
23+
24+
private final WxCpTpService mainService;
25+
26+
@Override
27+
public WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException {
28+
String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_SEND)
29+
+ "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
30+
return WxCpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
31+
}
32+
33+
@Override
34+
public WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException {
35+
String url = mainService.getWxCpTpConfigStorage().getApiUrl(GET_STATISTICS)
36+
+ "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
37+
return WxCpMessageSendStatistics.fromJson(
38+
this.mainService.post(url, WxCpGsonBuilder.create().toJson(ImmutableMap.of("time_type", timeType)), true));
39+
}
40+
41+
@Override
42+
public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId)
43+
throws WxErrorException {
44+
String url = mainService.getWxCpTpConfigStorage().getApiUrl(LINKEDCORP_MESSAGE_SEND)
45+
+ "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
46+
return WxCpLinkedCorpMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
47+
}
48+
49+
@Override
50+
public WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId)
51+
throws WxErrorException {
52+
String url = mainService.getWxCpTpConfigStorage().getApiUrl(EXTERNAL_CONTACT_MESSAGE_SEND)
53+
+ "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
54+
return WxCpSchoolContactMessageSendResult.fromJson(this.mainService.post(url, message.toJson(), true));
55+
}
56+
57+
@Override
58+
public void recall(String msgId, String corpId) throws WxErrorException {
59+
JsonObject jsonObject = new JsonObject();
60+
jsonObject.addProperty("msgid", msgId);
61+
String url = mainService.getWxCpTpConfigStorage().getApiUrl(MESSAGE_RECALL)
62+
+ "?access_token=" + mainService.getWxCpTpConfigStorage().getAccessToken(corpId);
63+
this.mainService.post(url, jsonObject.toString(), true);
64+
}
65+
66+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package me.chanjar.weixin.cp.tp.service.impl;
2+
3+
import me.chanjar.weixin.common.error.WxErrorException;
4+
import me.chanjar.weixin.cp.bean.message.WxCpMessage;
5+
import me.chanjar.weixin.cp.bean.message.WxCpMessageSendResult;
6+
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
7+
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
8+
import me.chanjar.weixin.cp.tp.service.WxCpTpMessageService;
9+
import org.mockito.Mock;
10+
import org.mockito.MockitoAnnotations;
11+
import org.testng.annotations.AfterClass;
12+
import org.testng.annotations.BeforeClass;
13+
import org.testng.annotations.Test;
14+
15+
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_RECALL;
16+
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message.MESSAGE_SEND;
17+
import static org.mockito.ArgumentMatchers.anyString;
18+
import static org.mockito.ArgumentMatchers.contains;
19+
import static org.mockito.ArgumentMatchers.eq;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.when;
22+
import static org.testng.Assert.assertNotNull;
23+
24+
/**
25+
* 企业微信第三方应用消息推送服务测试.
26+
*
27+
* @author GitHub Copilot
28+
*/
29+
public class WxCpTpMessageServiceImplTest {
30+
31+
@Mock
32+
private WxCpTpServiceApacheHttpClientImpl wxCpTpService;
33+
34+
@Mock
35+
private WxCpTpConfigStorage configStorage;
36+
37+
private WxCpTpMessageService wxCpTpMessageService;
38+
39+
private AutoCloseable mockitoAnnotations;
40+
41+
/**
42+
* Sets up.
43+
*/
44+
@BeforeClass
45+
public void setUp() {
46+
mockitoAnnotations = MockitoAnnotations.openMocks(this);
47+
when(wxCpTpService.getWxCpTpConfigStorage()).thenReturn(configStorage);
48+
WxCpTpDefaultConfigImpl defaultConfig = new WxCpTpDefaultConfigImpl();
49+
when(configStorage.getApiUrl(anyString()))
50+
.thenAnswer(invocation -> defaultConfig.getApiUrl(invocation.getArgument(0)));
51+
wxCpTpMessageService = new WxCpTpMessageServiceImpl(wxCpTpService);
52+
}
53+
54+
/**
55+
* Tear down.
56+
*
57+
* @throws Exception the exception
58+
*/
59+
@AfterClass
60+
public void tearDown() throws Exception {
61+
mockitoAnnotations.close();
62+
}
63+
64+
/**
65+
* 测试 send 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求.
66+
*
67+
* @throws WxErrorException 微信错误异常
68+
*/
69+
@Test
70+
public void testSendMessage() throws WxErrorException {
71+
String corpId = "test_corp_id";
72+
String accessToken = "test_access_token";
73+
String mockResponse = "{\"errcode\":0,\"errmsg\":\"ok\",\"msgid\":\"msg_001\"}";
74+
75+
when(configStorage.getAccessToken(corpId)).thenReturn(accessToken);
76+
String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_SEND)
77+
+ "?access_token=" + accessToken;
78+
when(wxCpTpService.post(eq(expectedUrl), anyString(), eq(true))).thenReturn(mockResponse);
79+
80+
WxCpMessage message = WxCpMessage.TEXT().toUser("zhangsan").content("hello").agentId(1).build();
81+
WxCpMessageSendResult result = wxCpTpMessageService.send(message, corpId);
82+
assertNotNull(result);
83+
84+
// 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token
85+
verify(wxCpTpService).post(eq(expectedUrl), anyString(), eq(true));
86+
}
87+
88+
/**
89+
* 测试 recall 方法:验证使用了 corpId 对应的 access_token,并以 withoutSuiteAccessToken=true 发起请求.
90+
*
91+
* @throws WxErrorException 微信错误异常
92+
*/
93+
@Test
94+
public void testRecallMessage() throws WxErrorException {
95+
String corpId = "test_corp_id";
96+
String accessToken = "test_access_token";
97+
String msgId = "test_msg_id";
98+
99+
when(configStorage.getAccessToken(corpId)).thenReturn(accessToken);
100+
String expectedUrl = new WxCpTpDefaultConfigImpl().getApiUrl(MESSAGE_RECALL)
101+
+ "?access_token=" + accessToken;
102+
when(wxCpTpService.post(eq(expectedUrl), contains(msgId), eq(true))).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\"}");
103+
104+
wxCpTpMessageService.recall(msgId, corpId);
105+
106+
// 验证调用时传入了 withoutSuiteAccessToken=true,确保不会附加 suite_access_token
107+
verify(wxCpTpService).post(eq(expectedUrl), contains(msgId), eq(true));
108+
}
109+
110+
/**
111+
* 测试 getWxCpTpMessageService 方法:验证 BaseWxCpTpServiceImpl 中正确初始化了消息服务.
112+
*/
113+
@Test
114+
public void testGetWxCpTpMessageServiceFromBase() {
115+
WxCpTpServiceApacheHttpClientImpl tpService = new WxCpTpServiceApacheHttpClientImpl();
116+
assertNotNull(tpService.getWxCpTpMessageService());
117+
}
118+
}

weixin-java-cp/src/test/resources/testng.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImplTest"/>
1010
<class name="me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImplTest"/>
1111
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpTagServiceImplTest"/>
12+
<class name="me.chanjar.weixin.cp.tp.service.impl.WxCpTpMessageServiceImplTest"/>
1213
</classes>
1314
</test>
1415

0 commit comments

Comments
 (0)