dev --- 3.8.0.A版本, openProject引用 ; formao-live --- 3.7.0.B 版本, formallProject引用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
9.6 KiB

  1. package chanjarster.weixin.api;
  2. import java.io.File;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. import java.security.MessageDigest;
  6. import java.util.Arrays;
  7. import java.util.UUID;
  8. import java.util.concurrent.atomic.AtomicBoolean;
  9. import org.apache.commons.lang3.StringUtils;
  10. import org.apache.http.client.ClientProtocolException;
  11. import org.apache.http.client.methods.CloseableHttpResponse;
  12. import org.apache.http.client.methods.HttpGet;
  13. import org.apache.http.impl.client.BasicResponseHandler;
  14. import org.apache.http.impl.client.CloseableHttpClient;
  15. import org.apache.http.impl.client.HttpClients;
  16. import chanjarster.weixin.bean.WxAccessToken;
  17. import chanjarster.weixin.bean.WxCustomMessage;
  18. import chanjarster.weixin.bean.WxMassGroupMessage;
  19. import chanjarster.weixin.bean.WxMassNews;
  20. import chanjarster.weixin.bean.WxMassOpenIdsMessage;
  21. import chanjarster.weixin.bean.WxMassVideo;
  22. import chanjarster.weixin.bean.WxMenu;
  23. import chanjarster.weixin.bean.result.WxError;
  24. import chanjarster.weixin.bean.result.WxMassSendResult;
  25. import chanjarster.weixin.bean.result.WxMassUploadResult;
  26. import chanjarster.weixin.bean.result.WxMediaUploadResult;
  27. import chanjarster.weixin.exception.WxErrorException;
  28. import chanjarster.weixin.util.fs.FileUtil;
  29. import chanjarster.weixin.util.http.MediaDownloadRequestExecutor;
  30. import chanjarster.weixin.util.http.MediaUploadRequestExecutor;
  31. import chanjarster.weixin.util.http.RequestExecutor;
  32. import chanjarster.weixin.util.http.SimpleGetRequestExecutor;
  33. import chanjarster.weixin.util.http.SimplePostRequestExecutor;
  34. public class WxServiceImpl implements WxService {
  35. /**
  36. * 全局的是否正在刷新Access Token的flag
  37. * true: 正在刷新
  38. * false: 没有刷新
  39. */
  40. protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false);
  41. protected static final CloseableHttpClient httpclient = HttpClients.createDefault();
  42. protected WxConfigStorage wxConfigStorage;
  43. protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>();
  44. public boolean checkSignature(String timestamp, String nonce, String signature) {
  45. try {
  46. String token = wxConfigStorage.getToken();
  47. MessageDigest sha1 = MessageDigest.getInstance("SHA1");
  48. String[] arr = new String[] { token, timestamp, nonce };
  49. Arrays.sort(arr);
  50. StringBuilder sb = new StringBuilder();
  51. for(String a : arr) {
  52. sb.append(a);
  53. }
  54. sha1.update(sb.toString().getBytes());
  55. byte[] output = sha1.digest();
  56. return bytesToHex(output).equals(signature);
  57. } catch (Exception e) {
  58. return false;
  59. }
  60. }
  61. protected String bytesToHex(byte[] b) {
  62. char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
  63. '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
  64. StringBuffer buf = new StringBuffer();
  65. for (int j = 0; j < b.length; j++) {
  66. buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
  67. buf.append(hexDigit[b[j] & 0x0f]);
  68. }
  69. return buf.toString();
  70. }
  71. public void accessTokenRefresh() throws WxErrorException {
  72. if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) {
  73. try {
  74. String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
  75. + "&appid=" + wxConfigStorage.getAppId()
  76. + "&secret=" + wxConfigStorage.getSecret()
  77. ;
  78. try {
  79. HttpGet httpGet = new HttpGet(url);
  80. CloseableHttpResponse response = httpclient.execute(httpGet);
  81. String resultContent = new BasicResponseHandler().handleResponse(response);
  82. WxError error = WxError.fromJson(resultContent);
  83. if (error.getErrcode() != 0) {
  84. throw new WxErrorException(error);
  85. }
  86. WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
  87. wxConfigStorage.updateAccessToken(accessToken.getAccess_token(), accessToken.getExpires_in());
  88. } catch (ClientProtocolException e) {
  89. throw new RuntimeException(e);
  90. } catch (IOException e) {
  91. throw new RuntimeException(e);
  92. }
  93. } finally {
  94. GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false);
  95. }
  96. } else {
  97. // 每隔100ms检查一下是否刷新完毕了
  98. while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) {
  99. try {
  100. Thread.sleep(100);
  101. } catch (InterruptedException e) {
  102. }
  103. }
  104. // 刷新完毕了,就没他什么事儿了
  105. }
  106. }
  107. public void customMessageSend(WxCustomMessage message) throws WxErrorException {
  108. String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
  109. execute(new SimplePostRequestExecutor(), url, message.toJson());
  110. }
  111. public void menuCreate(WxMenu menu) throws WxErrorException {
  112. String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
  113. execute(new SimplePostRequestExecutor(), url, menu.toJson());
  114. }
  115. public void menuDelete() throws WxErrorException {
  116. String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
  117. execute(new SimpleGetRequestExecutor(), url, null);
  118. }
  119. public WxMenu menuGet() throws WxErrorException {
  120. String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
  121. try {
  122. String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
  123. return WxMenu.fromJson(resultContent);
  124. } catch (WxErrorException e) {
  125. // 46003 不存在的菜单数据
  126. if (e.getError().getErrcode() == 46003) {
  127. return null;
  128. }
  129. throw e;
  130. }
  131. }
  132. public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException {
  133. return mediaUpload(mediaType,FileUtil.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
  134. }
  135. public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
  136. String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
  137. return execute(new MediaUploadRequestExecutor(), url, file);
  138. }
  139. public File mediaDownload(String media_id) throws WxErrorException {
  140. String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
  141. return execute(new MediaDownloadRequestExecutor(), url, "media_id=" + media_id);
  142. }
  143. public WxMassUploadResult massNewsUpload(WxMassNews news) throws WxErrorException {
  144. String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews";
  145. String responseContent = execute(new SimplePostRequestExecutor(), url, news.toJson());
  146. return WxMassUploadResult.fromJson(responseContent);
  147. }
  148. public WxMassUploadResult massVideoUpload(WxMassVideo video) throws WxErrorException {
  149. String url = "http://file.api.weixin.qq.com/cgi-bin/media/uploadvideo";
  150. String responseContent = execute(new SimplePostRequestExecutor(), url, video.toJson());
  151. return WxMassUploadResult.fromJson(responseContent);
  152. }
  153. public WxMassSendResult massGroupMessageSend(WxMassGroupMessage message) throws WxErrorException {
  154. String url = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall";
  155. String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
  156. return WxMassSendResult.fromJson(responseContent);
  157. }
  158. public WxMassSendResult massOpenIdsMessageSend(WxMassOpenIdsMessage message) throws WxErrorException {
  159. String url = "https://api.weixin.qq.com/cgi-bin/message/mass/send";
  160. String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
  161. return WxMassSendResult.fromJson(responseContent);
  162. }
  163. /**
  164. * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
  165. * @param executor
  166. * @param uri
  167. * @param data
  168. * @return
  169. * @throws WxErrorException
  170. */
  171. public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
  172. if (StringUtils.isBlank(wxConfigStorage.getAccessToken())) {
  173. accessTokenRefresh();
  174. }
  175. String accessToken = wxConfigStorage.getAccessToken();
  176. String uriWithAccessToken = uri;
  177. uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;
  178. try {
  179. return executor.execute(uriWithAccessToken, data);
  180. } catch (WxErrorException e) {
  181. WxError error = e.getError();
  182. /*
  183. * 发生以下情况时尝试刷新access_token
  184. * 40001 获取access_token时AppSecret错误,或者access_token无效
  185. * 42001 access_token超时
  186. */
  187. if (error.getErrcode() == 42001 || error.getErrcode() == 40001) {
  188. accessTokenRefresh();
  189. return execute(executor, uri, data);
  190. }
  191. /**
  192. * -1 系统繁忙, 1000ms后重试
  193. */
  194. if (error.getErrcode() == -1) {
  195. if(retryTimes.get() == null) {
  196. retryTimes.set(0);
  197. }
  198. if (retryTimes.get() > 5) {
  199. retryTimes.set(0);
  200. throw new RuntimeException("微信服务端异常,超出重试次数");
  201. }
  202. int sleepMillis = 1000 * (1 >> (retryTimes.get() - 1));
  203. try {
  204. System.out.println("微信系统繁忙," + sleepMillis + "ms后重试");
  205. Thread.sleep(sleepMillis);
  206. retryTimes.set(retryTimes.get() + 1);
  207. return execute(executor, uri, data);
  208. } catch (InterruptedException e1) {
  209. throw new RuntimeException(e1);
  210. }
  211. }
  212. if (error.getErrcode() != 0) {
  213. throw new WxErrorException(error);
  214. }
  215. return null;
  216. } catch (ClientProtocolException e) {
  217. throw new RuntimeException(e);
  218. } catch (IOException e) {
  219. throw new RuntimeException(e);
  220. }
  221. }
  222. public void setWxConfigStorage(WxConfigStorage wxConfigProvider) {
  223. this.wxConfigStorage = wxConfigProvider;
  224. }
  225. }