EnterpriseWechatApi.java 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package com.qqflow.engine.domain.ewechat.api;
  2. import cn.hutool.core.io.FileUtil;
  3. import cn.hutool.core.map.MapBuilder;
  4. import cn.hutool.core.map.MapUtil;
  5. import com.qqflow.engine.common.exception.BusinessException;
  6. import com.qqflow.engine.common.util.Assert;
  7. import com.qqflow.engine.domain.ewechat.model.enterprise.req.*;
  8. import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.*;
  9. import lombok.RequiredArgsConstructor;
  10. import org.springframework.core.io.ClassPathResource;
  11. import org.springframework.core.io.FileSystemResource;
  12. import org.springframework.core.io.Resource;
  13. import org.springframework.http.*;
  14. import org.springframework.stereotype.Component;
  15. import org.springframework.util.LinkedMultiValueMap;
  16. import org.springframework.util.MultiValueMap;
  17. import org.springframework.web.client.RestTemplate;
  18. import org.springframework.web.multipart.MultipartFile;
  19. import java.io.File;
  20. import java.net.URI;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Objects;
  24. import java.util.Optional;
  25. /**
  26. * @author WuWenYi
  27. * @since 2025/3/17
  28. */
  29. @Component
  30. @RequiredArgsConstructor
  31. public class EnterpriseWechatApi {
  32. private static final String BASE_URL = "https://qyapi.weixin.qq.com";
  33. private final RestTemplate restTemplate;
  34. private void checkRsp(EnterpriseWechatBaseRsp rsp){
  35. Assert.notNull(rsp,"返回值为空");
  36. boolean condition = Objects.equals(null, rsp.getErrcode()) || Objects.equals(rsp.getErrcode(), 0);
  37. Assert.isTure(condition, rsp.getErrmsg());
  38. }
  39. /**
  40. * 获取企业微信后台推送的ticket
  41. * suiteId 第三方应用id或者代开发应用模板id。第三方应用以ww或wx开头应用id(对应于旧的以tj开头的套件id);代开发应用以dk开头
  42. * @param suiteTicket suite_ticket由企业微信后台定时推送给“指令回调URL”,每十分钟更新一次,见推送suite_ticket。
  43. * @return 第三方应用凭证(用于获取永久授权码)
  44. */
  45. public EnterpriseWechatSuiteAccessTokenRsp getSuiteAccessToken(String suiteId, String suiteSecret, String suiteTicket) {
  46. EnterpriseWechatSuiteAccessTokenReq req = EnterpriseWechatSuiteAccessTokenReq.of(suiteId, suiteSecret, suiteTicket);
  47. return this.commonPost("/cgi-bin/service/get_suite_token", req, EnterpriseWechatSuiteAccessTokenRsp.class);
  48. }
  49. /** 获取授权信息 */
  50. public EnterpriseWechatPermanentCodeRsp getPermanentCode(String suiteAccessToken, String authCode){
  51. Map<String, Object> req = MapBuilder.<String,Object>create().put("auth_code", authCode).build();
  52. String uri = "/cgi-bin/service/get_permanent_code?suite_access_token=" + suiteAccessToken;
  53. return this.commonPost(uri, req, EnterpriseWechatPermanentCodeRsp.class);
  54. }
  55. /** 获取accesstoken,基本上要调代开发的接口都需要用到这个 */
  56. public EnterpriseWechatAccessTokenRsp getAccessToken(String corpId, String permanentCode) {
  57. String uri = "/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + permanentCode;
  58. return this.commonGet(uri, EnterpriseWechatAccessTokenRsp.class);
  59. }
  60. /** 获取成员ID列表(open_userid + 部门ID),支持分页游标 */
  61. public EnterpriseWechatListIdRsp listUserId(String accessToken, String cursor, Integer limit) {
  62. EnterpriseWechatListIdReq req = new EnterpriseWechatListIdReq();
  63. req.setCursor(cursor);
  64. req.setLimit(limit);
  65. String uri = "/cgi-bin/user/list_id?access_token=" + accessToken;
  66. return this.commonPost(uri, req, EnterpriseWechatListIdRsp.class);
  67. }
  68. /** 获取部门列表(递归获取指定部门及其子部门,不填id则获取全量) */
  69. public EnterpriseWechatDepartmentListRsp listDepartment(String accessToken, Long deptId) {
  70. String uri = "/cgi-bin/department/list?access_token=" + accessToken;
  71. if (deptId != null) uri += "&id=" + deptId;
  72. return this.commonGet(uri, EnterpriseWechatDepartmentListRsp.class);
  73. }
  74. /** 获取部门成员(仅返回本部门直属成员,不含子部门) */
  75. public EnterpriseWechatUserListRsp listDepartmentUsers(String accessToken, Long departmentId) {
  76. String uri = "/cgi-bin/user/simplelist?access_token=" + accessToken + "&department_id=" + departmentId;
  77. return this.commonGet(uri, EnterpriseWechatUserListRsp.class);
  78. }
  79. /** 通过手机号获取userid */
  80. public EnterpriseWechatUseridByMobileRsp getUseridByMobile(String accessToken, String mobile) {
  81. EnterpriseWechatUseridByMobileReq req = EnterpriseWechatUseridByMobileReq.of(mobile);
  82. String uri = "/cgi-bin/user/getuserid?access_token=" + accessToken;
  83. return this.commonPost(uri, req, EnterpriseWechatUseridByMobileRsp.class);
  84. }
  85. /**
  86. * 添加客服
  87. * @param name 客服名称
  88. * @param mediaId 素材ID
  89. */
  90. public EnterpriseWechatAddCsRsp addCs(String accessToken, String name, String mediaId) {
  91. EnterpriseWechatAddCsReq req = EnterpriseWechatAddCsReq.of(name, mediaId);
  92. String uri = "/cgi-bin/kf/account/add?access_token=" + accessToken;
  93. return this.commonPost(uri, req, EnterpriseWechatAddCsRsp.class);
  94. }
  95. public EnterpriseWechatCsUrlRsp getCsUrl(String accessToken, String csId) {
  96. EnterpriseWechatCsUrlReq req = EnterpriseWechatCsUrlReq.of(csId, "test1");
  97. String uri = "/cgi-bin/kf/add_contact_way?access_token=" + accessToken;
  98. return this.commonPost(uri, req, EnterpriseWechatCsUrlRsp.class);
  99. }
  100. public EnterpriseWechatBaseRsp deleteCs(String accessToken, String openKfid) {
  101. String uri = "/cgi-bin/kf/account/del?access_token=" + accessToken;
  102. Map<String, String> req = MapUtil.builder("open_kfid", openKfid).build();
  103. return this.commonPost(uri, req, EnterpriseWechatBaseRsp.class);
  104. }
  105. /** 同步消息 */
  106. public EnterpriseWechatSyncMsgRsp syncMsg(String accessToken, String cursor, String token, String openKfId) {
  107. EnterpriseWechatSyncMsgReq req = EnterpriseWechatSyncMsgReq.of(cursor, token, openKfId);
  108. String uri = "/cgi-bin/kf/sync_msg?access_token=" + accessToken;
  109. return this.commonPost(uri, req, EnterpriseWechatSyncMsgRsp.class);
  110. }
  111. /** 获取会话状态 */
  112. public EnterpriseWechatSessionRsp getSessionState(String accessToken, String openKfId, String userId){
  113. EnterpriseWechatSessionReq req = EnterpriseWechatSessionReq.of(openKfId, userId);
  114. String uri = "/cgi-bin/kf/service_state/get?access_token=" + accessToken;
  115. return this.commonPost(uri, req, EnterpriseWechatSessionRsp.class);
  116. }
  117. /** 变更会话状态 */
  118. public EnterpriseWechatChangeSessionRsp changeSessionState(String accessToken, String openKfId, String userId, Integer serviceState){
  119. EnterpriseWechatChangeSessionReq req = EnterpriseWechatChangeSessionReq.of(openKfId, userId, serviceState);
  120. String uri = "/cgi-bin/kf/service_state/trans?access_token=" + accessToken;
  121. return this.commonPost(uri, req, EnterpriseWechatChangeSessionRsp.class);
  122. }
  123. /** 发送回应消息 */
  124. public EnterpriseWechatSendMsgRsp sendMsgOnEvent(String accessToken, String code, String content){
  125. EnterpriseWechatSendOnEventMsgReq req = EnterpriseWechatSendOnEventMsgReq.of(code, content);
  126. String uri = "/cgi-bin/kf/send_msg_on_event?access_token=" + accessToken;
  127. return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
  128. }
  129. /** 发送客服消息 */
  130. public EnterpriseWechatSendMsgRsp sendKfMsg(String accessToken, EnterpriseWechatSendMsgReq req){
  131. String uri = "/cgi-bin/kf/send_msg?access_token=" + accessToken;
  132. return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
  133. }
  134. /** 发送应用文本消息 */
  135. public EnterpriseWechatSendTextMsgRsp sendTextMsg(String accessToken, EnterpriseWechatSendTextMsgReq req) {
  136. String uri = "/cgi-bin/message/send?access_token=" + accessToken;
  137. return this.commonPost(uri, req, EnterpriseWechatSendTextMsgRsp.class);
  138. }
  139. /** 上传素材文件 */
  140. public EnterpriseWechatUploadRsp upload(String accessToken, String type, File file) {
  141. return this.upload(accessToken, type, getResource(file.getPath()), (int) file.length());
  142. }
  143. public EnterpriseWechatUploadRsp upload(String accessToken, String type, MultipartFile multipartFile) {
  144. return this.upload(accessToken, type, multipartFile.getResource(), (int) multipartFile.getSize());
  145. }
  146. private EnterpriseWechatUploadRsp upload(String accessToken, String type, Resource resource, int fileLength) {
  147. String url = BASE_URL + "/cgi-bin/media/upload?access_token=" + accessToken + "&type=" + type;
  148. MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  149. body.add("file", resource);
  150. HttpHeaders headers = new HttpHeaders();
  151. headers.set("Content-disposition", "form-data; name=media;filename=" + resource.getFilename() + "; filelength=" + fileLength);
  152. headers.setContentType(MediaType.MULTIPART_FORM_DATA);
  153. HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
  154. ResponseEntity<EnterpriseWechatUploadRsp> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, EnterpriseWechatUploadRsp.class);
  155. this.checkRsp(response.getBody());
  156. return response.getBody();
  157. }
  158. private static Resource getResource(String path) {
  159. // 先尝试从 classpath 加载
  160. Resource resource = new ClassPathResource(path);
  161. if (resource.exists()) {
  162. return resource;
  163. }
  164. // 如果不存在,再尝试从文件系统加载
  165. File file = new File(path);
  166. if (file.exists()) {
  167. return new FileSystemResource(file);
  168. }
  169. throw new IllegalArgumentException("File not found: " + path);
  170. }
  171. /** 下载临时素材 */
  172. public String downloadMedia(String accessToken, String mediaId) {
  173. String url = BASE_URL + "/cgi-bin/media/get?access_token=" + accessToken + "&media_id=" + mediaId;
  174. HttpHeaders headers = new HttpHeaders();
  175. headers.set(HttpHeaders.USER_AGENT, "Mozilla/5.0");
  176. RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(url));
  177. ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
  178. Assert.isTure(response.getStatusCode() == HttpStatus.OK, "download media failed");
  179. // 获取 Content-Disposition 头
  180. HttpHeaders responseHeaders = response.getHeaders();
  181. String contentType = responseHeaders.getContentType() != null ? responseHeaders.getContentType().toString() : "";
  182. if (contentType.contains("application/json")) {
  183. throw new BusinessException("download media failed,error:" + new String(response.getBody()));
  184. }
  185. String fileName = extractFileName(responseHeaders).orElseThrow(() -> new BusinessException("can not get file name"));
  186. // 保存文件
  187. FileUtil.writeBytes(response.getBody(),fileName);
  188. return fileName;
  189. }
  190. private static Optional<String> extractFileName(HttpHeaders headers) {
  191. List<String> dispositions = headers.get(HttpHeaders.CONTENT_DISPOSITION);
  192. if (dispositions != null) {
  193. for (String disposition : dispositions) {
  194. if (disposition.contains("filename=")) {
  195. return Optional.of(disposition.split("filename=")[1].replaceAll("\"", ""));
  196. }
  197. }
  198. }
  199. return Optional.empty();
  200. }
  201. private <T extends EnterpriseWechatBaseRsp> T commonPost(String uri, Object req, Class<T> rspClass) {
  202. String url = BASE_URL + uri;
  203. HttpHeaders headers = new HttpHeaders();
  204. headers.setContentType(MediaType.APPLICATION_JSON);
  205. HttpEntity<Object> requestEntity = new HttpEntity<>(req, headers);
  206. T rsp = restTemplate.postForObject(url, requestEntity, rspClass);
  207. this.checkRsp(rsp);
  208. return rsp;
  209. }
  210. private <T extends EnterpriseWechatBaseRsp> T commonGet(String uri, Class<T> rspClass) {
  211. String url = BASE_URL + uri;
  212. HttpHeaders headers = new HttpHeaders();
  213. headers.setContentType(MediaType.APPLICATION_JSON);
  214. HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
  215. ResponseEntity<T> exchange = restTemplate.exchange(url, HttpMethod.GET, requestEntity, rspClass);
  216. this.checkRsp(exchange.getBody());
  217. return exchange.getBody();
  218. }
  219. }