com.wallellen.wechat.mp.api.WxMpServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.wallellen.wechat.mp.api.WxMpServiceImpl.java

Source

/*
 * This file Copyright (c) 2016. Walle.
 * (http://www.wallellen.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the
 * Walle Agreement (WA) and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or WA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Walle Agreement (WA), this file
 * and the accompanying materials are made available under the
 * terms of the WA which accompanies this distribution, and
 * is available at http://www.wallellen.com/agreement.html
 *
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 */

package com.wallellen.wechat.mp.api;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.thoughtworks.xstream.XStream;
import com.wallellen.wechat.common.api.WxConsts;
import com.wallellen.wechat.common.bean.WxAccessToken;
import com.wallellen.wechat.common.bean.WxJsapiSignature;
import com.wallellen.wechat.common.bean.WxMenu;
import com.wallellen.wechat.common.bean.result.WxError;
import com.wallellen.wechat.common.bean.result.WxMediaUploadResult;
import com.wallellen.wechat.common.exception.WxErrorException;
import com.wallellen.wechat.common.session.StandardSessionManager;
import com.wallellen.wechat.common.session.WxSessionManager;
import com.wallellen.wechat.common.util.RandomUtils;
import com.wallellen.wechat.common.util.StringUtils;
import com.wallellen.wechat.common.util.crypto.SHA1;
import com.wallellen.wechat.common.util.crypto.WxCryptUtil;
import com.wallellen.wechat.common.util.fs.FileUtils;
import com.wallellen.wechat.common.util.http.MediaDownloadRequestExecutor;
import com.wallellen.wechat.common.util.http.MediaUploadRequestExecutor;
import com.wallellen.wechat.common.util.http.RequestExecutor;
import com.wallellen.wechat.common.util.http.SimpleGetRequestExecutor;
import com.wallellen.wechat.common.util.http.SimplePostRequestExecutor;
import com.wallellen.wechat.common.util.http.URIUtil;
import com.wallellen.wechat.common.util.http.Utf8ResponseHandler;
import com.wallellen.wechat.common.util.json.GsonHelper;
import com.wallellen.wechat.common.util.json.WxGsonBuilder;
import com.wallellen.wechat.common.util.xml.XStreamInitializer;
import com.wallellen.wechat.mp.bean.WxMpCustomMessage;
import com.wallellen.wechat.mp.bean.WxMpGroup;
import com.wallellen.wechat.mp.bean.WxMpMassGroupMessage;
import com.wallellen.wechat.mp.bean.WxMpMassNews;
import com.wallellen.wechat.mp.bean.WxMpMassOpenIdsMessage;
import com.wallellen.wechat.mp.bean.WxMpMassVideo;
import com.wallellen.wechat.mp.bean.WxMpMaterial;
import com.wallellen.wechat.mp.bean.WxMpMaterialArticleUpdate;
import com.wallellen.wechat.mp.bean.WxMpMaterialNews;
import com.wallellen.wechat.mp.bean.WxMpSemanticQuery;
import com.wallellen.wechat.mp.bean.WxMpTemplateMessage;
import com.wallellen.wechat.mp.bean.result.WxMpMassSendResult;
import com.wallellen.wechat.mp.bean.result.WxMpMassUploadResult;
import com.wallellen.wechat.mp.bean.result.WxMpMaterialCountResult;
import com.wallellen.wechat.mp.bean.result.WxMpMaterialFileBatchGetResult;
import com.wallellen.wechat.mp.bean.result.WxMpMaterialNewsBatchGetResult;
import com.wallellen.wechat.mp.bean.result.WxMpMaterialUploadResult;
import com.wallellen.wechat.mp.bean.result.WxMpMaterialVideoInfoResult;
import com.wallellen.wechat.mp.bean.result.WxMpOAuth2AccessToken;
import com.wallellen.wechat.mp.bean.result.WxMpPayCallback;
import com.wallellen.wechat.mp.bean.result.WxMpPayResult;
import com.wallellen.wechat.mp.bean.result.WxMpPrepayIdResult;
import com.wallellen.wechat.mp.bean.result.WxMpQrCodeTicket;
import com.wallellen.wechat.mp.bean.result.WxMpSemanticQueryResult;
import com.wallellen.wechat.mp.bean.result.WxMpUser;
import com.wallellen.wechat.mp.bean.result.WxMpUserCumulate;
import com.wallellen.wechat.mp.bean.result.WxMpUserList;
import com.wallellen.wechat.mp.bean.result.WxMpUserSummary;
import com.wallellen.wechat.mp.bean.result.WxRedpackResult;
import com.wallellen.wechat.mp.util.http.MaterialDeleteRequestExecutor;
import com.wallellen.wechat.mp.util.http.MaterialNewsInfoRequestExecutor;
import com.wallellen.wechat.mp.util.http.MaterialUploadRequestExecutor;
import com.wallellen.wechat.mp.util.http.MaterialVideoInfoRequestExecutor;
import com.wallellen.wechat.mp.util.http.MaterialVoiceAndImageDownloadRequestExecutor;
import com.wallellen.wechat.mp.util.http.QrCodeRequestExecutor;
import com.wallellen.wechat.mp.util.json.WxMpGsonBuilder;
import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;

public class WxMpServiceImpl implements WxMpService {

    protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class);

    /**
     * ?access token?
     */
    protected final Object globalAccessTokenRefreshLock = new Object();

    /**
     * ?jsapi_ticket?
     */
    protected final Object globalJsapiTicketRefreshLock = new Object();
    final String[] REQUIRED_ORDER_PARAMETERS = new String[] { "appid", "mch_id", "body", "out_trade_no",
            "total_fee", "spbill_create_ip", "notify_url", "trade_type", };
    protected WxMpConfigStorage wxMpConfigStorage;
    protected CloseableHttpClient httpClient;
    protected HttpHost httpProxy;
    protected WxSessionManager sessionManager = new StandardSessionManager();
    private int retrySleepMillis = 1000;
    private int maxRetryTimes = 5;

    public boolean checkSignature(String timestamp, String nonce, String signature) {
        try {
            return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature);
        } catch (Exception e) {
            return false;
        }
    }

    public String getAccessToken() throws WxErrorException {
        return getAccessToken(false);
    }

    public String getAccessToken(boolean forceRefresh) throws WxErrorException {
        if (forceRefresh) {
            wxMpConfigStorage.expireAccessToken();
        }
        if (wxMpConfigStorage.isAccessTokenExpired()) {
            synchronized (globalAccessTokenRefreshLock) {
                if (wxMpConfigStorage.isAccessTokenExpired()) {
                    String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + "&appid="
                            + wxMpConfigStorage.getAppId() + "&secret=" + wxMpConfigStorage.getSecret();
                    try {
                        HttpGet httpGet = new HttpGet(url);
                        if (httpProxy != null) {
                            RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
                            httpGet.setConfig(config);
                        }
                        CloseableHttpResponse response = getHttpclient().execute(httpGet);
                        String resultContent = new BasicResponseHandler().handleResponse(response);
                        WxError error = WxError.fromJson(resultContent);
                        if (error.getErrorCode() != 0) {
                            throw new WxErrorException(error);
                        }
                        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
                        wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(),
                                accessToken.getExpiresIn());
                    } catch (ClientProtocolException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return wxMpConfigStorage.getAccessToken();
    }

    public String getJsapiTicket() throws WxErrorException {
        return getJsapiTicket(false);
    }

    public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
        if (forceRefresh) {
            wxMpConfigStorage.expireJsapiTicket();
        }
        if (wxMpConfigStorage.isJsapiTicketExpired()) {
            synchronized (globalJsapiTicketRefreshLock) {
                if (wxMpConfigStorage.isJsapiTicketExpired()) {
                    String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi";
                    String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
                    JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
                    JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject();
                    String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
                    int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
                    wxMpConfigStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds);
                }
            }
        }
        return wxMpConfigStorage.getJsapiTicket();
    }

    public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
        long timestamp = System.currentTimeMillis() / 1000;
        String noncestr = RandomUtils.getRandomStr();
        String jsapiTicket = getJsapiTicket(false);
        try {
            String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + noncestr,
                    "timestamp=" + timestamp, "url=" + url);
            WxJsapiSignature jsapiSignature = new WxJsapiSignature();
            jsapiSignature.setAppid(wxMpConfigStorage.getAppId());
            jsapiSignature.setTimestamp(timestamp);
            jsapiSignature.setNoncestr(noncestr);
            jsapiSignature.setUrl(url);
            jsapiSignature.setSignature(signature);
            return jsapiSignature;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public void customMessageSend(WxMpCustomMessage message) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
        execute(new SimplePostRequestExecutor(), url, message.toJson());
    }

    public void menuCreate(WxMenu menu) throws WxErrorException {
        if (menu.getMatchRule() != null) {
            String url = "https://api.weixin.qq.com/cgi-bin/menu/addconditional";
            execute(new SimplePostRequestExecutor(), url, menu.toJson());
        } else {
            String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
            execute(new SimplePostRequestExecutor(), url, menu.toJson());
        }
    }

    public void menuDelete() throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
        execute(new SimpleGetRequestExecutor(), url, null);
    }

    public void menuDelete(String menuid) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/menu/delconditional";
        execute(new SimpleGetRequestExecutor(), url, "menuid=" + menuid);
    }

    public WxMenu menuGet() throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
        try {
            String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
            return WxMenu.fromJson(resultContent);
        } catch (WxErrorException e) {
            // 46003 ????
            if (e.getError().getErrorCode() == 46003) {
                return null;
            }
            throw e;
        }
    }

    public WxMenu menuTryMatch(String userid) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/menu/trymatch";
        try {
            String resultContent = execute(new SimpleGetRequestExecutor(), url, "user_id=" + userid);
            return WxMenu.fromJson(resultContent);
        } catch (WxErrorException e) {
            // 46003 ????     46002 ???
            if (e.getError().getErrorCode() == 46003 || e.getError().getErrorCode() == 46002) {
                return null;
            }
            throw e;
        }
    }

    public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
            throws WxErrorException, IOException {
        return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
    }

    public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
        String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
        return execute(new MediaUploadRequestExecutor(), url, file);
    }

    public File mediaDownload(String media_id) throws WxErrorException {
        String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
        return execute(new MediaDownloadRequestExecutor(wxMpConfigStorage.getTmpDirFile()), url,
                "media_id=" + media_id);
    }

    public WxMpMaterialUploadResult materialFileUpload(String mediaType, WxMpMaterial material)
            throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/add_material?type=" + mediaType;
        return execute(new MaterialUploadRequestExecutor(), url, material);
    }

    public WxMpMaterialUploadResult materialNewsUpload(WxMpMaterialNews news) throws WxErrorException {
        if (news == null || news.isEmpty()) {
            throw new IllegalArgumentException("news is empty!");
        }
        String url = "https://api.weixin.qq.com/cgi-bin/material/add_news";
        String responseContent = post(url, news.toJson());
        return WxMpMaterialUploadResult.fromJson(responseContent);
    }

    public InputStream materialImageOrVoiceDownload(String media_id) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/get_material";
        return execute(new MaterialVoiceAndImageDownloadRequestExecutor(wxMpConfigStorage.getTmpDirFile()), url,
                media_id);
    }

    public WxMpMaterialVideoInfoResult materialVideoInfo(String media_id) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/get_material";
        return execute(new MaterialVideoInfoRequestExecutor(), url, media_id);
    }

    public WxMpMaterialNews materialNewsInfo(String media_id) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/get_material";
        return execute(new MaterialNewsInfoRequestExecutor(), url, media_id);
    }

    public boolean materialNewsUpdate(WxMpMaterialArticleUpdate wxMpMaterialArticleUpdate) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/update_news";
        String responseText = post(url, wxMpMaterialArticleUpdate.toJson());
        WxError wxError = WxError.fromJson(responseText);
        if (wxError.getErrorCode() == 0) {
            return true;
        } else {
            throw new WxErrorException(wxError);
        }
    }

    public boolean materialDelete(String media_id) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/del_material";
        return execute(new MaterialDeleteRequestExecutor(), url, media_id);
    }

    public WxMpMaterialCountResult materialCount() throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount";
        String responseText = get(url, null);
        WxError wxError = WxError.fromJson(responseText);
        if (wxError.getErrorCode() == 0) {
            return WxMpGsonBuilder.create().fromJson(responseText, WxMpMaterialCountResult.class);
        } else {
            throw new WxErrorException(wxError);
        }
    }

    public WxMpMaterialNewsBatchGetResult materialNewsBatchGet(int offset, int count) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/batchget_material";
        Map<String, Object> params = new HashMap<>();
        params.put("type", WxConsts.MATERIAL_NEWS);
        params.put("offset", offset);
        params.put("count", count);
        String responseText = post(url, WxGsonBuilder.create().toJson(params));
        WxError wxError = WxError.fromJson(responseText);
        if (wxError.getErrorCode() == 0) {
            return WxMpGsonBuilder.create().fromJson(responseText, WxMpMaterialNewsBatchGetResult.class);
        } else {
            throw new WxErrorException(wxError);
        }
    }

    public WxMpMaterialFileBatchGetResult materialFileBatchGet(String type, int offset, int count)
            throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/material/batchget_material";
        Map<String, Object> params = new HashMap<>();
        params.put("type", type);
        params.put("offset", offset);
        params.put("count", count);
        String responseText = post(url, WxGsonBuilder.create().toJson(params));
        WxError wxError = WxError.fromJson(responseText);
        if (wxError.getErrorCode() == 0) {
            return WxMpGsonBuilder.create().fromJson(responseText, WxMpMaterialFileBatchGetResult.class);
        } else {
            throw new WxErrorException(wxError);
        }
    }

    public WxMpMassUploadResult massNewsUpload(WxMpMassNews news) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews";
        String responseContent = execute(new SimplePostRequestExecutor(), url, news.toJson());
        return WxMpMassUploadResult.fromJson(responseContent);
    }

    public WxMpMassUploadResult massVideoUpload(WxMpMassVideo video) throws WxErrorException {
        String url = "http://file.api.weixin.qq.com/cgi-bin/media/uploadvideo";
        String responseContent = execute(new SimplePostRequestExecutor(), url, video.toJson());
        return WxMpMassUploadResult.fromJson(responseContent);
    }

    public WxMpMassSendResult massGroupMessageSend(WxMpMassGroupMessage message) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall";
        String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
        return WxMpMassSendResult.fromJson(responseContent);
    }

    public WxMpMassSendResult massOpenIdsMessageSend(WxMpMassOpenIdsMessage message) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/message/mass/send";
        String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
        return WxMpMassSendResult.fromJson(responseContent);
    }

    public WxMpGroup groupCreate(String name) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/groups/create";
        JsonObject json = new JsonObject();
        JsonObject groupJson = new JsonObject();
        json.add("group", groupJson);
        groupJson.addProperty("name", name);

        String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
        return WxMpGroup.fromJson(responseContent);
    }

    public List<WxMpGroup> groupGet() throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/groups/get";
        String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
        /*
         * ?API { group : { id : ..., name : ...} }
         *  { groups : [ { id : ..., name : ..., count : ... }, ... ] }
         */
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("groups"),
                new TypeToken<List<WxMpGroup>>() {
                }.getType());
    }

    public long userGetGroup(String openid) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/groups/getid";
        JsonObject o = new JsonObject();
        o.addProperty("openid", openid);
        String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        return GsonHelper.getAsLong(tmpJsonElement.getAsJsonObject().get("groupid"));
    }

    public void groupUpdate(WxMpGroup group) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/groups/update";
        execute(new SimplePostRequestExecutor(), url, group.toJson());
    }

    public void userUpdateGroup(String openid, long to_groupid) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/groups/members/update";
        JsonObject json = new JsonObject();
        json.addProperty("openid", openid);
        json.addProperty("to_groupid", to_groupid);
        execute(new SimplePostRequestExecutor(), url, json.toString());
    }

    public void userUpdateRemark(String openid, String remark) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark";
        JsonObject json = new JsonObject();
        json.addProperty("openid", openid);
        json.addProperty("remark", remark);
        execute(new SimplePostRequestExecutor(), url, json.toString());
    }

    public WxMpUser userInfo(String openid, String lang) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/user/info";
        lang = lang == null ? "zh_CN" : lang;
        String responseContent = execute(new SimpleGetRequestExecutor(), url, "openid=" + openid + "&lang=" + lang);
        return WxMpUser.fromJson(responseContent);
    }

    public WxMpUserList userList(String next_openid) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/user/get";
        String responseContent = execute(new SimpleGetRequestExecutor(), url,
                next_openid == null ? null : "next_openid=" + next_openid);
        return WxMpUserList.fromJson(responseContent);
    }

    public WxMpQrCodeTicket qrCodeCreateTmpTicket(int scene_id, Integer expire_seconds) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
        JsonObject json = new JsonObject();
        json.addProperty("action_name", "QR_SCENE");
        if (expire_seconds != null) {
            json.addProperty("expire_seconds", expire_seconds);
        }
        JsonObject actionInfo = new JsonObject();
        JsonObject scene = new JsonObject();
        scene.addProperty("scene_id", scene_id);
        actionInfo.add("scene", scene);
        json.add("action_info", actionInfo);
        String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
        return WxMpQrCodeTicket.fromJson(responseContent);
    }

    public WxMpQrCodeTicket qrCodeCreateLastTicket(int scene_id) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
        JsonObject json = new JsonObject();
        json.addProperty("action_name", "QR_LIMIT_SCENE");
        JsonObject actionInfo = new JsonObject();
        JsonObject scene = new JsonObject();
        scene.addProperty("scene_id", scene_id);
        actionInfo.add("scene", scene);
        json.add("action_info", actionInfo);
        String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
        return WxMpQrCodeTicket.fromJson(responseContent);
    }

    public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
        JsonObject json = new JsonObject();
        json.addProperty("action_name", "QR_LIMIT_STR_SCENE");
        JsonObject actionInfo = new JsonObject();
        JsonObject scene = new JsonObject();
        scene.addProperty("scene_str", scene_str);
        actionInfo.add("scene", scene);
        json.add("action_info", actionInfo);
        String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
        return WxMpQrCodeTicket.fromJson(responseContent);
    }

    public File qrCodePicture(WxMpQrCodeTicket ticket) throws WxErrorException {
        String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode";
        return execute(new QrCodeRequestExecutor(), url, ticket);
    }

    public String shortUrl(String long_url) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/shorturl";
        JsonObject o = new JsonObject();
        o.addProperty("action", "long2short");
        o.addProperty("long_url", long_url);
        String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        return tmpJsonElement.getAsJsonObject().get("short_url").getAsString();
    }

    public String templateSend(WxMpTemplateMessage templateMessage) throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send";
        String responseContent = execute(new SimplePostRequestExecutor(), url, templateMessage.toJson());
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        final JsonObject jsonObject = tmpJsonElement.getAsJsonObject();
        if (jsonObject.get("errcode").getAsInt() == 0)
            return jsonObject.get("msgid").getAsString();
        throw new WxErrorException(WxError.fromJson(responseContent));
    }

    public WxMpSemanticQueryResult semanticQuery(WxMpSemanticQuery semanticQuery) throws WxErrorException {
        String url = "https://api.weixin.qq.com/semantic/semproxy/search";
        String responseContent = execute(new SimplePostRequestExecutor(), url, semanticQuery.toJson());
        return WxMpSemanticQueryResult.fromJson(responseContent);
    }

    @Override
    public String oauth2buildAuthorizationUrl(String scope, String state) {
        return this.oauth2buildAuthorizationUrl(wxMpConfigStorage.getOauth2redirectUri(), scope, state);
    }

    @Override
    public String oauth2buildAuthorizationUrl(String redirectURI, String scope, String state) {
        String url = "https://open.weixin.qq.com/connect/oauth2/authorize?";
        url += "appid=" + wxMpConfigStorage.getAppId();
        url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectURI);
        url += "&response_type=code";
        url += "&scope=" + scope;
        if (state != null) {
            url += "&state=" + state;
        }
        url += "#wechat_redirect";
        return url;
    }

    @Override
    public WxMpOAuth2AccessToken oauth2getAccessToken(String code) throws WxErrorException {
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?";
        url += "appid=" + wxMpConfigStorage.getAppId();
        url += "&secret=" + wxMpConfigStorage.getSecret();
        url += "&code=" + code;
        url += "&grant_type=authorization_code";

        try {
            RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
            String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
            return WxMpOAuth2AccessToken.fromJson(responseText);
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public WxMpOAuth2AccessToken oauth2refreshAccessToken(String refreshToken) throws WxErrorException {
        String url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?";
        url += "appid=" + wxMpConfigStorage.getAppId();
        url += "&grant_type=refresh_token";
        url += "&refresh_token=" + refreshToken;

        try {
            RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
            String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
            return WxMpOAuth2AccessToken.fromJson(responseText);
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public WxMpUser oauth2getUserInfo(WxMpOAuth2AccessToken oAuth2AccessToken, String lang)
            throws WxErrorException {
        String url = "https://api.weixin.qq.com/sns/userinfo?";
        url += "access_token=" + oAuth2AccessToken.getAccessToken();
        url += "&openid=" + oAuth2AccessToken.getOpenId();
        if (lang == null) {
            url += "&lang=zh_CN";
        } else {
            url += "&lang=" + lang;
        }

        try {
            RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
            String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
            return WxMpUser.fromJson(responseText);
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean oauth2validateAccessToken(WxMpOAuth2AccessToken oAuth2AccessToken) {
        String url = "https://api.weixin.qq.com/sns/auth?";
        url += "access_token=" + oAuth2AccessToken.getAccessToken();
        url += "&openid=" + oAuth2AccessToken.getOpenId();

        try {
            RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
            executor.execute(getHttpclient(), httpProxy, url, null);
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (WxErrorException e) {
            return false;
        }
        return true;
    }

    @Override
    public String[] getCallbackIP() throws WxErrorException {
        String url = "https://api.weixin.qq.com/cgi-bin/getcallbackip";
        String responseContent = get(url, null);
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        JsonArray ipList = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray();
        String[] ipArray = new String[ipList.size()];
        for (int i = 0; i < ipList.size(); i++) {
            ipArray[i] = ipList.get(i).getAsString();
        }
        return ipArray;
    }

    @Override
    public List<WxMpUserSummary> getUserSummary(Date beginDate, Date endDate) throws WxErrorException {
        String url = "https://api.weixin.qq.com/datacube/getusersummary";
        JsonObject param = new JsonObject();
        param.addProperty("begin_date", SIMPLE_DATE_FORMAT.format(beginDate));
        param.addProperty("end_date", SIMPLE_DATE_FORMAT.format(endDate));
        String responseContent = post(url, param.toString());
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("list"),
                new TypeToken<List<WxMpUserSummary>>() {
                }.getType());
    }

    @Override
    public List<WxMpUserCumulate> getUserCumulate(Date beginDate, Date endDate) throws WxErrorException {
        String url = "https://api.weixin.qq.com/datacube/getusercumulate";
        JsonObject param = new JsonObject();
        param.addProperty("begin_date", SIMPLE_DATE_FORMAT.format(beginDate));
        param.addProperty("end_date", SIMPLE_DATE_FORMAT.format(endDate));
        String responseContent = post(url, param.toString());
        JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
        return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("list"),
                new TypeToken<List<WxMpUserCumulate>>() {
                }.getType());
    }

    public String get(String url, String queryParam) throws WxErrorException {
        return execute(new SimpleGetRequestExecutor(), url, queryParam);
    }

    public String post(String url, String postData) throws WxErrorException {
        return execute(new SimplePostRequestExecutor(), url, postData);
    }

    /**
     * ????access_token????
     *
     * @param executor
     * @param uri
     * @param data
     * @return
     * @throws WxErrorException
     */
    public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
        int retryTimes = 0;
        do {
            try {
                return executeInternal(executor, uri, data);
            } catch (WxErrorException e) {
                WxError error = e.getError();
                /**
                 * -1 ?, 1000ms??
                 */
                if (error.getErrorCode() == -1) {
                    int sleepMillis = retrySleepMillis * (1 << retryTimes);
                    try {
                        log.debug("?{}ms ??({})", sleepMillis, retryTimes + 1);
                        Thread.sleep(sleepMillis);
                    } catch (InterruptedException e1) {
                        throw new RuntimeException(e1);
                    }
                } else {
                    throw e;
                }
            }
        } while (++retryTimes < maxRetryTimes);

        throw new RuntimeException("??");
    }

    protected synchronized <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data)
            throws WxErrorException {
        if (uri.indexOf("access_token=") != -1) {
            throw new IllegalArgumentException("uri???access_token: " + uri);
        }
        String accessToken = getAccessToken(false);

        String uriWithAccessToken = uri;
        uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken
                : "&access_token=" + accessToken;

        try {
            return executor.execute(getHttpclient(), httpProxy, uriWithAccessToken, data);
        } catch (WxErrorException e) {
            WxError error = e.getError();
            /*
             * ??access_token
             * 40001 ?access_tokenAppSecretaccess_token
             * 42001 access_token
             */
            if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) {
                // wxMpConfigStorageaccess tokenaccess token
                wxMpConfigStorage.expireAccessToken();
                return execute(executor, uri, data);
            }
            if (error.getErrorCode() != 0) {
                throw new WxErrorException(error);
            }
            return null;
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected CloseableHttpClient getHttpclient() {
        return httpClient;
    }

    public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider) {
        this.wxMpConfigStorage = wxConfigProvider;

        String http_proxy_host = wxMpConfigStorage.getHttp_proxy_host();
        int http_proxy_port = wxMpConfigStorage.getHttp_proxy_port();
        String http_proxy_username = wxMpConfigStorage.getHttp_proxy_username();
        String http_proxy_password = wxMpConfigStorage.getHttp_proxy_password();

        final HttpClientBuilder builder = HttpClients.custom();
        if (StringUtils.isNotBlank(http_proxy_host)) {
            // ??
            if (StringUtils.isNotBlank(http_proxy_username)) {
                // ????
                CredentialsProvider credsProvider = new BasicCredentialsProvider();
                credsProvider.setCredentials(new AuthScope(http_proxy_host, http_proxy_port),
                        new UsernamePasswordCredentials(http_proxy_username, http_proxy_password));
                builder.setDefaultCredentialsProvider(credsProvider);
            } else {
                // ???
            }
            httpProxy = new HttpHost(http_proxy_host, http_proxy_port);
        }
        if (wxConfigProvider.getSSLContext() != null) {
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(wxConfigProvider.getSSLContext(),
                    new String[] { "TLSv1" }, null,
                    SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
            builder.setSSLSocketFactory(sslsf);
        }
        httpClient = builder.build();
    }

    @Override
    public void setRetrySleepMillis(int retrySleepMillis) {
        this.retrySleepMillis = retrySleepMillis;
    }

    @Override
    public void setMaxRetryTimes(int maxRetryTimes) {
        this.maxRetryTimes = maxRetryTimes;
    }

    @Override
    public WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, double amt, String body,
            String tradeType, String ip, String callbackUrl) {
        Map<String, String> packageParams = new HashMap<>();
        packageParams.put("appid", wxMpConfigStorage.getAppId());
        packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
        packageParams.put("body", body);
        packageParams.put("out_trade_no", outTradeNo);
        packageParams.put("total_fee", (int) (amt * 100) + "");
        packageParams.put("spbill_create_ip", ip);
        packageParams.put("notify_url", callbackUrl);
        packageParams.put("trade_type", tradeType);
        packageParams.put("openid", openId);

        return getPrepayId(packageParams);
    }

    public WxMpPrepayIdResult getPrepayId(final Map<String, String> parameters) {
        String nonce_str = System.currentTimeMillis() + "";

        final SortedMap<String, String> packageParams = new TreeMap<>(parameters);
        packageParams.put("appid", wxMpConfigStorage.getAppId());
        packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
        packageParams.put("nonce_str", nonce_str);
        checkParameters(packageParams);

        String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey());
        packageParams.put("sign", sign);

        StringBuilder request = new StringBuilder("<xml>");
        for (Entry<String, String> para : packageParams.entrySet()) {
            request.append(String.format("<%s>%s</%s>", para.getKey(), para.getValue(), para.getKey()));
        }
        request.append("</xml>");

        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder");
        if (httpProxy != null) {
            RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
            httpPost.setConfig(config);
        }

        StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8);
        httpPost.setEntity(entity);
        try {
            CloseableHttpResponse response = getHttpclient().execute(httpPost);
            String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
            XStream xstream = XStreamInitializer.getInstance();
            xstream.alias("xml", WxMpPrepayIdResult.class);
            return (WxMpPrepayIdResult) xstream.fromXML(responseContent);
        } catch (IOException e) {
            throw new RuntimeException("Failed to get prepay id due to IO exception.", e);
        }
    }

    private void checkParameters(Map<String, String> parameters) {
        for (String para : REQUIRED_ORDER_PARAMETERS) {
            if (!parameters.containsKey(para))
                throw new IllegalArgumentException("Reqiured argument '" + para + "' is missing.");
        }
        if ("JSAPI".equals(parameters.get("trade_type")) && !parameters.containsKey("openid"))
            throw new IllegalArgumentException("Reqiured argument 'openid' is missing when trade_type is 'JSAPI'.");
        if ("NATIVE".equals(parameters.get("trade_type")) && !parameters.containsKey("product_id"))
            throw new IllegalArgumentException(
                    "Reqiured argument 'product_id' is missing when trade_type is 'NATIVE'.");
    }

    @Override
    public Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body,
            String tradeType, String ip, String callbackUrl) {
        Map<String, String> packageParams = new HashMap<>();
        packageParams.put("appid", wxMpConfigStorage.getAppId());
        packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
        packageParams.put("body", body);
        packageParams.put("out_trade_no", outTradeNo);
        packageParams.put("total_fee", (int) (amt * 100) + "");
        packageParams.put("spbill_create_ip", ip);
        packageParams.put("notify_url", callbackUrl);
        packageParams.put("trade_type", tradeType);
        packageParams.put("openid", openId);

        return getJSSDKPayInfo(packageParams);
    }

    @Override
    public Map<String, String> getJSSDKPayInfo(Map<String, String> parameters) {
        WxMpPrepayIdResult wxMpPrepayIdResult = getPrepayId(parameters);
        String prepayId = wxMpPrepayIdResult.getPrepay_id();
        if (prepayId == null || prepayId.equals("")) {
            throw new RuntimeException(String.format("Failed to get prepay id due to error code '%s'(%s).",
                    wxMpPrepayIdResult.getErr_code(), wxMpPrepayIdResult.getErr_code_des()));
        }

        Map<String, String> payInfo = new HashMap<>();
        payInfo.put("appId", wxMpConfigStorage.getAppId());
        // ???jssdktimestamp???????timeStamp??S
        payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
        payInfo.put("nonceStr", System.currentTimeMillis() + "");
        payInfo.put("package", "prepay_id=" + prepayId);
        payInfo.put("signType", "MD5");

        String finalSign = WxCryptUtil.createSign(payInfo, wxMpConfigStorage.getPartnerKey());
        payInfo.put("paySign", finalSign);
        return payInfo;
    }

    @Override
    public WxMpPayResult getJSSDKPayResult(String transactionId, String outTradeNo) {
        String nonce_str = System.currentTimeMillis() + "";

        SortedMap<String, String> packageParams = new TreeMap<>();
        packageParams.put("appid", wxMpConfigStorage.getAppId());
        packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
        if (transactionId != null && !"".equals(transactionId.trim()))
            packageParams.put("transaction_id", transactionId);
        else if (outTradeNo != null && !"".equals(outTradeNo.trim()))
            packageParams.put("out_trade_no", outTradeNo);
        else
            throw new IllegalArgumentException("Either 'transactionId' or 'outTradeNo' must be given.");
        packageParams.put("nonce_str", nonce_str);
        packageParams.put("sign", WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey()));

        StringBuilder request = new StringBuilder("<xml>");
        for (Entry<String, String> para : packageParams.entrySet()) {
            request.append(String.format("<%s>%s</%s>", para.getKey(), para.getValue(), para.getKey()));
        }
        request.append("</xml>");

        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/orderquery");
        if (httpProxy != null) {
            RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
            httpPost.setConfig(config);
        }

        StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8);
        httpPost.setEntity(entity);
        try {
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
            XStream xstream = XStreamInitializer.getInstance();
            xstream.alias("xml", WxMpPayResult.class);
            return (WxMpPayResult) xstream.fromXML(responseContent);
        } catch (IOException e) {
            throw new RuntimeException("Failed to query order due to IO exception.", e);
        }
    }

    @Override
    public WxMpPayCallback getJSSDKCallbackData(String xmlData) {
        try {
            XStream xstream = XStreamInitializer.getInstance();
            xstream.alias("xml", WxMpPayCallback.class);
            return (WxMpPayCallback) xstream.fromXML(xmlData);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new WxMpPayCallback();
    }

    @Override
    public boolean checkJSSDKCallbackDataSignature(Map<String, String> kvm, String signature) {
        return signature.equals(WxCryptUtil.createSign(kvm, wxMpConfigStorage.getPartnerKey()));
    }

    @Override
    public WxRedpackResult sendRedpack(Map<String, String> parameters) throws WxErrorException {
        String nonce_str = System.currentTimeMillis() + "";

        SortedMap<String, String> packageParams = new TreeMap<>(parameters);
        packageParams.put("wxappid", wxMpConfigStorage.getAppId());
        packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
        packageParams.put("nonce_str", nonce_str);

        String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey());
        packageParams.put("sign", sign);

        StringBuilder request = new StringBuilder("<xml>");
        for (Entry<String, String> para : packageParams.entrySet()) {
            request.append(String.format("<%s>%s</%s>", para.getKey(), para.getValue(), para.getKey()));
        }
        request.append("</xml>");

        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
        if (httpProxy != null) {
            RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
            httpPost.setConfig(config);
        }

        StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8);
        httpPost.setEntity(entity);
        try {
            CloseableHttpResponse response = getHttpclient().execute(httpPost);
            String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
            XStream xstream = XStreamInitializer.getInstance();
            xstream.processAnnotations(WxRedpackResult.class);
            return (WxRedpackResult) xstream.fromXML(responseContent);
        } catch (IOException e) {
            log.error(MessageFormatter
                    .format("The exception was happened when sending redpack '{}'.", request.toString())
                    .getMessage(), e);
            WxError error = new WxError();
            error.setErrorCode(-1);
            throw new WxErrorException(error);
        }
    }
}