dev --- 3.8.0.A版本, openProject引用 ; formao-live --- 3.7.0.B 版本, formallProject引用
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

380 lignes
15 KiB

  1. package me.chanjar.weixin.api;
  2. import java.io.File;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. import java.io.StringReader;
  6. import java.security.MessageDigest;
  7. import java.util.Arrays;
  8. import java.util.List;
  9. import java.util.UUID;
  10. import java.util.concurrent.atomic.AtomicBoolean;
  11. import me.chanjar.weixin.bean.WxCustomMessage;
  12. import me.chanjar.weixin.util.json.GsonHelper;
  13. import org.apache.commons.lang3.StringUtils;
  14. import org.apache.http.client.ClientProtocolException;
  15. import org.apache.http.client.methods.CloseableHttpResponse;
  16. import org.apache.http.client.methods.HttpGet;
  17. import org.apache.http.impl.client.BasicResponseHandler;
  18. import org.apache.http.impl.client.CloseableHttpClient;
  19. import org.apache.http.impl.client.HttpClients;
  20. import me.chanjar.weixin.bean.WxAccessToken;
  21. import me.chanjar.weixin.bean.WxGroup;
  22. import me.chanjar.weixin.bean.WxMassGroupMessage;
  23. import me.chanjar.weixin.bean.WxMassNews;
  24. import me.chanjar.weixin.bean.WxMassOpenIdsMessage;
  25. import me.chanjar.weixin.bean.WxMassVideo;
  26. import me.chanjar.weixin.bean.WxMenu;
  27. import me.chanjar.weixin.bean.result.WxError;
  28. import me.chanjar.weixin.bean.result.WxMassSendResult;
  29. import me.chanjar.weixin.bean.result.WxMassUploadResult;
  30. import me.chanjar.weixin.bean.result.WxMediaUploadResult;
  31. import me.chanjar.weixin.bean.result.WxQrCodeTicket;
  32. import me.chanjar.weixin.bean.result.WxUser;
  33. import me.chanjar.weixin.bean.result.WxUserList;
  34. import me.chanjar.weixin.exception.WxErrorException;
  35. import me.chanjar.weixin.util.fs.FileUtils;
  36. import me.chanjar.weixin.util.http.MediaDownloadRequestExecutor;
  37. import me.chanjar.weixin.util.http.MediaUploadRequestExecutor;
  38. import me.chanjar.weixin.util.http.QrCodeRequestExecutor;
  39. import me.chanjar.weixin.util.http.RequestExecutor;
  40. import me.chanjar.weixin.util.http.SimpleGetRequestExecutor;
  41. import me.chanjar.weixin.util.http.SimplePostRequestExecutor;
  42. import me.chanjar.weixin.util.json.WxGsonBuilder;
  43. import com.google.gson.JsonElement;
  44. import com.google.gson.JsonObject;
  45. import com.google.gson.internal.Streams;
  46. import com.google.gson.reflect.TypeToken;
  47. import com.google.gson.stream.JsonReader;
  48. public class WxServiceImpl implements WxService {
  49. /**
  50. * 全局的是否正在刷新Access Token的flag
  51. * true: 正在刷新
  52. * false: 没有刷新
  53. */
  54. protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false);
  55. protected static final CloseableHttpClient httpclient = HttpClients.createDefault();
  56. protected WxConfigStorage wxConfigStorage;
  57. protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>();
  58. public boolean checkSignature(String timestamp, String nonce, String signature) {
  59. try {
  60. String token = wxConfigStorage.getToken();
  61. MessageDigest sha1 = MessageDigest.getInstance("SHA1");
  62. String[] arr = new String[] { token, timestamp, nonce };
  63. Arrays.sort(arr);
  64. StringBuilder sb = new StringBuilder();
  65. for(String a : arr) {
  66. sb.append(a);
  67. }
  68. sha1.update(sb.toString().getBytes());
  69. byte[] output = sha1.digest();
  70. return bytesToHex(output).equals(signature);
  71. } catch (Exception e) {
  72. return false;
  73. }
  74. }
  75. protected String bytesToHex(byte[] b) {
  76. char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
  77. '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
  78. StringBuffer buf = new StringBuffer();
  79. for (int j = 0; j < b.length; j++) {
  80. buf.append(hexDigit[(b[j] >> 4) & 0x0f]);
  81. buf.append(hexDigit[b[j] & 0x0f]);
  82. }
  83. return buf.toString();
  84. }
  85. public void accessTokenRefresh() throws WxErrorException {
  86. if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) {
  87. try {
  88. String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
  89. + "&appid=" + wxConfigStorage.getAppId()
  90. + "&secret=" + wxConfigStorage.getSecret()
  91. ;
  92. try {
  93. HttpGet httpGet = new HttpGet(url);
  94. CloseableHttpResponse response = httpclient.execute(httpGet);
  95. String resultContent = new BasicResponseHandler().handleResponse(response);
  96. WxError error = WxError.fromJson(resultContent);
  97. if (error.getErrcode() != 0) {
  98. throw new WxErrorException(error);
  99. }
  100. WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
  101. wxConfigStorage.updateAccessToken(accessToken.getAccess_token(), accessToken.getExpires_in());
  102. } catch (ClientProtocolException e) {
  103. throw new RuntimeException(e);
  104. } catch (IOException e) {
  105. throw new RuntimeException(e);
  106. }
  107. } finally {
  108. GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false);
  109. }
  110. } else {
  111. // 每隔100ms检查一下是否刷新完毕了
  112. while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) {
  113. try {
  114. Thread.sleep(100);
  115. } catch (InterruptedException e) {
  116. }
  117. }
  118. // 刷新完毕了,就没他什么事儿了
  119. }
  120. }
  121. public void customMessageSend(WxCustomMessage message) throws WxErrorException {
  122. String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
  123. execute(new SimplePostRequestExecutor(), url, message.toJson());
  124. }
  125. public void menuCreate(WxMenu menu) throws WxErrorException {
  126. String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
  127. execute(new SimplePostRequestExecutor(), url, menu.toJson());
  128. }
  129. public void menuDelete() throws WxErrorException {
  130. String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
  131. execute(new SimpleGetRequestExecutor(), url, null);
  132. }
  133. public WxMenu menuGet() throws WxErrorException {
  134. String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
  135. try {
  136. String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
  137. return WxMenu.fromJson(resultContent);
  138. } catch (WxErrorException e) {
  139. // 46003 不存在的菜单数据
  140. if (e.getError().getErrcode() == 46003) {
  141. return null;
  142. }
  143. throw e;
  144. }
  145. }
  146. public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException {
  147. return mediaUpload(mediaType,FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
  148. }
  149. public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
  150. String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
  151. return execute(new MediaUploadRequestExecutor(), url, file);
  152. }
  153. public File mediaDownload(String media_id) throws WxErrorException {
  154. String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
  155. return execute(new MediaDownloadRequestExecutor(), url, "media_id=" + media_id);
  156. }
  157. public WxMassUploadResult massNewsUpload(WxMassNews news) throws WxErrorException {
  158. String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews";
  159. String responseContent = execute(new SimplePostRequestExecutor(), url, news.toJson());
  160. return WxMassUploadResult.fromJson(responseContent);
  161. }
  162. public WxMassUploadResult massVideoUpload(WxMassVideo video) throws WxErrorException {
  163. String url = "http://file.api.weixin.qq.com/cgi-bin/media/uploadvideo";
  164. String responseContent = execute(new SimplePostRequestExecutor(), url, video.toJson());
  165. return WxMassUploadResult.fromJson(responseContent);
  166. }
  167. public WxMassSendResult massGroupMessageSend(WxMassGroupMessage message) throws WxErrorException {
  168. String url = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall";
  169. String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
  170. return WxMassSendResult.fromJson(responseContent);
  171. }
  172. public WxMassSendResult massOpenIdsMessageSend(WxMassOpenIdsMessage message) throws WxErrorException {
  173. String url = "https://api.weixin.qq.com/cgi-bin/message/mass/send";
  174. String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
  175. return WxMassSendResult.fromJson(responseContent);
  176. }
  177. public WxGroup groupCreate(String name) throws WxErrorException {
  178. String url = "https://api.weixin.qq.com/cgi-bin/groups/create";
  179. JsonObject json = new JsonObject();
  180. JsonObject groupJson = new JsonObject();
  181. json.add("group", groupJson);
  182. groupJson.addProperty("name", name);
  183. String responseContent = execute(
  184. new SimplePostRequestExecutor(),
  185. url,
  186. json.toString());
  187. return WxGroup.fromJson(responseContent);
  188. }
  189. public List<WxGroup> groupGet() throws WxErrorException {
  190. String url = "https://api.weixin.qq.com/cgi-bin/groups/get";
  191. String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
  192. /*
  193. * 操蛋的微信API,创建时返回的是 { group : { id : ..., name : ...} }
  194. * 查询时返回的是 { groups : [ { id : ..., name : ..., count : ... }, ... ] }
  195. */
  196. JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
  197. return WxGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("groups"), new TypeToken<List<WxGroup>>(){}.getType());
  198. }
  199. public long userGetGroup(String openid) throws WxErrorException {
  200. String url = "https://api.weixin.qq.com/cgi-bin/groups/getid";
  201. JsonObject o = new JsonObject();
  202. o.addProperty("openid", openid);
  203. String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
  204. JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
  205. return GsonHelper.getAsLong(tmpJsonElement.getAsJsonObject().get("groupid"));
  206. }
  207. public void groupUpdate(WxGroup group) throws WxErrorException {
  208. String url = "https://api.weixin.qq.com/cgi-bin/groups/update";
  209. execute(new SimplePostRequestExecutor(), url, group.toJson());
  210. }
  211. public void userUpdateGroup(String openid, long to_groupid) throws WxErrorException {
  212. String url = "https://api.weixin.qq.com/cgi-bin/groups/members/update";
  213. JsonObject json = new JsonObject();
  214. json.addProperty("openid", openid);
  215. json.addProperty("to_groupid", to_groupid);
  216. execute(new SimplePostRequestExecutor(), url, json.toString());
  217. }
  218. public void userUpdateRemark(String openid, String remark) throws WxErrorException {
  219. String url = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark";
  220. JsonObject json = new JsonObject();
  221. json.addProperty("openid", openid);
  222. json.addProperty("remark", remark);
  223. execute(new SimplePostRequestExecutor(), url, json.toString());
  224. }
  225. public WxUser userInfo(String openid, String lang) throws WxErrorException {
  226. String url = "https://api.weixin.qq.com/cgi-bin/user/info";
  227. lang = lang == null ? "zh_CN" : lang;
  228. String responseContent = execute(new SimpleGetRequestExecutor(), url, "openid=" + openid + "&lang=" + lang);
  229. return WxUser.fromJson(responseContent);
  230. }
  231. public WxUserList userList(String next_openid) throws WxErrorException {
  232. String url = "https://api.weixin.qq.com/cgi-bin/user/get";
  233. String responseContent = execute(new SimpleGetRequestExecutor(), url, next_openid == null ? null : "next_openid=" + next_openid);
  234. return WxUserList.fromJson(responseContent);
  235. }
  236. public WxQrCodeTicket qrCodeCreateTmpTicket(int scene_id, Integer expire_seconds) throws WxErrorException {
  237. String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
  238. JsonObject json = new JsonObject();
  239. json.addProperty("action_name", "QR_SCENE");
  240. if(expire_seconds != null) {
  241. json.addProperty("expire_seconds", expire_seconds);
  242. }
  243. JsonObject actionInfo = new JsonObject();
  244. JsonObject scene = new JsonObject();
  245. scene.addProperty("scene_id", scene_id);
  246. actionInfo.add("scene", scene);
  247. json.add("action_info", actionInfo);
  248. String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
  249. return WxQrCodeTicket.fromJson(responseContent);
  250. }
  251. public WxQrCodeTicket qrCodeCreateLastTicket(int scene_id) throws WxErrorException {
  252. String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
  253. JsonObject json = new JsonObject();
  254. json.addProperty("action_name", "QR_LIMIT_SCENE");
  255. JsonObject actionInfo = new JsonObject();
  256. JsonObject scene = new JsonObject();
  257. scene.addProperty("scene_id", scene_id);
  258. actionInfo.add("scene", scene);
  259. json.add("action_info", actionInfo);
  260. String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
  261. return WxQrCodeTicket.fromJson(responseContent);
  262. }
  263. public File qrCodePicture(WxQrCodeTicket ticket) throws WxErrorException {
  264. String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode";
  265. return execute(new QrCodeRequestExecutor(), url, ticket);
  266. }
  267. public String shortUrl(String long_url) throws WxErrorException {
  268. String url = "https://api.weixin.qq.com/cgi-bin/shorturl";
  269. JsonObject o = new JsonObject();
  270. o.addProperty("action", "long2short");
  271. o.addProperty("long_url", long_url);
  272. String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
  273. JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
  274. return tmpJsonElement.getAsJsonObject().get("short_url").getAsString();
  275. }
  276. /**
  277. * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
  278. * @param executor
  279. * @param uri
  280. * @param data
  281. * @return
  282. * @throws WxErrorException
  283. */
  284. public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
  285. if (StringUtils.isBlank(wxConfigStorage.getAccessToken())) {
  286. accessTokenRefresh();
  287. }
  288. String accessToken = wxConfigStorage.getAccessToken();
  289. String uriWithAccessToken = uri;
  290. uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;
  291. try {
  292. return executor.execute(uriWithAccessToken, data);
  293. } catch (WxErrorException e) {
  294. WxError error = e.getError();
  295. /*
  296. * 发生以下情况时尝试刷新access_token
  297. * 40001 获取access_token时AppSecret错误,或者access_token无效
  298. * 42001 access_token超时
  299. */
  300. if (error.getErrcode() == 42001 || error.getErrcode() == 40001) {
  301. accessTokenRefresh();
  302. return execute(executor, uri, data);
  303. }
  304. /**
  305. * -1 系统繁忙, 1000ms后重试
  306. */
  307. if (error.getErrcode() == -1) {
  308. if(retryTimes.get() == null) {
  309. retryTimes.set(0);
  310. }
  311. if (retryTimes.get() > 4) {
  312. retryTimes.set(0);
  313. throw new RuntimeException("微信服务端异常,超出重试次数");
  314. }
  315. int sleepMillis = 1000 * (1 << retryTimes.get());
  316. try {
  317. System.out.println("微信系统繁忙," + sleepMillis + "ms后重试");
  318. Thread.sleep(sleepMillis);
  319. retryTimes.set(retryTimes.get() + 1);
  320. return execute(executor, uri, data);
  321. } catch (InterruptedException e1) {
  322. throw new RuntimeException(e1);
  323. }
  324. }
  325. if (error.getErrcode() != 0) {
  326. throw new WxErrorException(error);
  327. }
  328. return null;
  329. } catch (ClientProtocolException e) {
  330. throw new RuntimeException(e);
  331. } catch (IOException e) {
  332. throw new RuntimeException(e);
  333. }
  334. }
  335. public void setWxConfigStorage(WxConfigStorage wxConfigProvider) {
  336. this.wxConfigStorage = wxConfigProvider;
  337. }
  338. }