Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link

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:55
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpMessageService.java:71

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


/**
* <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
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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的锁.
*/
Expand Down Expand Up @@ -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;
Expand Down
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 + "?access_token=" 直接拼接 URL,若 getApiUrl(...) 在某些私有化/网关场景下已带查询参数,可能生成 ?? 导致请求失败;可以考虑按 uri.contains("?") ? "&" : "?" 的方式拼接以规避边界问题。

Severity: low

Other Locations
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java:35
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java:44
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java:52
  • weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpMessageServiceImpl.java:61

Fix This in Augment

🤖 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前单测只覆盖了 send/recallwithoutSuiteAccessToken=true 的断言;为了避免后续改动时遗漏,建议为 getStatistics/sendLinkedCorpMessage/sendSchoolContactMessage 也补充同类校验。

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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());
}
}
1 change: 1 addition & 0 deletions weixin-java-cp/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down