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.

333 lines
13 KiB

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