content: /*
   * 支付宝开放平台API
   * 支付宝开放平台v3协议文档
   *
   * The version of the OpenAPI document: 2025-08-26
   * 
   *
   * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
   * https://openapi-generator.tech
   * Do not edit the class manually.
   */
  
  
  package com.alipay.v3;
  
  import okhttp3.*;
  import okhttp3.internal.http.HttpMethod;
  import okhttp3.internal.sse.RealEventSource;
  import okhttp3.internal.tls.OkHostnameVerifier;
  import okhttp3.logging.HttpLoggingInterceptor;
  import okhttp3.logging.HttpLoggingInterceptor.Level;
  import okhttp3.sse.EventSource;
  import okhttp3.sse.EventSourceListener;
  import okio.Buffer;
  import okio.BufferedSink;
  import okio.Okio;
  
  import javax.net.ssl.*;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.UnsupportedEncodingException;
  import java.lang.reflect.Type;
  import java.net.URI;
  import java.net.URLConnection;
  import java.net.URLEncoder;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.security.GeneralSecurityException;
  import java.security.KeyStore;
  import java.security.SecureRandom;
  import java.security.cert.Certificate;
  import java.security.cert.CertificateException;
  import java.security.cert.CertificateFactory;
  import java.security.cert.X509Certificate;
  import java.text.DateFormat;
  import java.text.SimpleDateFormat;
  import java.time.LocalDate;
  import java.time.OffsetDateTime;
  import java.time.format.DateTimeFormatter;
  import java.util.*;
  import java.util.Map.Entry;
  import java.util.concurrent.TimeUnit;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import com.google.common.base.Strings;
  
  import com.alipay.v3.auth.Authentication;
  import com.alipay.v3.auth.HttpBasicAuth;
  import com.alipay.v3.auth.HttpBearerAuth;
  import com.alipay.v3.auth.ApiKeyAuth;
  import com.alipay.v3.util.AlipayConfigUtil;
  import com.alipay.v3.util.AntCertificationUtil;
  import com.alipay.v3.util.AlipayLogger;
  import com.alipay.v3.util.StreamCallback;
  import com.alipay.v3.util.model.AlipayConfig;
  import com.alipay.v3.util.model.ByteStreamWrapper;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  
  /**
   * <p>ApiClient class.</p>
   */
  public class ApiClient {
  
      private String basePath = "https://openapi.alipay.com";
      private boolean debugging = false;
      private Map<String, String> defaultHeaderMap = new HashMap<String, String>();
      private Map<String, String> defaultCookieMap = new HashMap<String, String>();
      private String tempFolderPath = null;
  
      private Map<String, Authentication> authentications;
  
      private DateFormat dateFormat;
      private DateFormat datetimeFormat;
      private boolean lenientDatetimeFormat;
      private int dateLength;
  
      private InputStream sslCaCert;
      private boolean verifyingSsl;
      private KeyManager[] keyManagers;
  
      private OkHttpClient httpClient;
      private JSON json;
  
      private HttpLoggingInterceptor loggingInterceptor;
  
      private AlipayConfigUtil alipayConfigUtil = new AlipayConfigUtil();
  
      /**
       * Basic constructor for ApiClient
       */
      public ApiClient() {
          init();
          initHttpClient();
  
          // Setup authentications (key: authentication name, value: authentication).
          // Prevent the authentications from being modified.
          authentications = Collections.unmodifiableMap(authentications);
      }
  
      /**
       * Basic constructor with custom OkHttpClient
       *
       * @param client a {@link okhttp3.OkHttpClient} object
       */
      public ApiClient(OkHttpClient client) {
          init();
  
          httpClient = client;
  
          // Setup authentications (key: authentication name, value: authentication).
          // Prevent the authentications from being modified.
          authentications = Collections.unmodifiableMap(authentications);
      }
  
      public void setAlipayConfig(AlipayConfig alipayConfig) throws ApiException {
          if (alipayConfig == null) {
              return;
          }
          if (!Strings.isNullOrEmpty(alipayConfig.getServerUrl())){
              basePath = alipayConfig.getServerUrl();
          }
          alipayConfigUtil.setAppId(alipayConfig.getAppId());
          alipayConfigUtil.setPrivateKey(alipayConfig.getPrivateKey());
          alipayConfigUtil.setAlipayPublicKey(alipayConfig.getAlipayPublicKey());
          //对称加密
          alipayConfigUtil.setEncryptType(alipayConfig.getEncryptType());
          alipayConfigUtil.setEncryptKey(alipayConfig.getEncryptKey());
  
          //公钥证书未设置，提前返回，跳过后续证书初始化内容
          if (Strings.isNullOrEmpty(alipayConfig.getAlipayPublicCertContent()) && Strings.isNullOrEmpty(alipayConfig.getAlipayPublicCertPath())) {
              return;
          }
          //读取根证书（用来校验本地支付宝公钥证书失效后自动从网关下载的新支付宝公钥证书是否有效）
          if (Strings.isNullOrEmpty(alipayConfig.getRootCertContent()) && Strings.isNullOrEmpty(alipayConfig.getRootCertPath())) {
              throw new ApiException("支付宝根证书路径和根证书内容为空，请填写支付宝根证书路径字段[rootCertPath]或根证书内容字符串字段[rootCertContent]");
          }
          String rootCertContent = Strings.isNullOrEmpty(alipayConfig.getRootCertContent()) ? AntCertificationUtil.readCertContent(alipayConfig.getRootCertPath()) : alipayConfig.getRootCertContent();
          //alipayRootCertSN根证书序列号
          String alipayRootCertSN = AntCertificationUtil.getRootCertSN(rootCertContent);
          if (Strings.isNullOrEmpty(alipayRootCertSN)) {
              throw new ApiException("AlipayRootCert Is Invalid，请检查支付宝根证书路径字段[rootCertPath]或根证书内容字符串字段[rootCertContent]对应的支付宝根证书是否正确");
          }
          alipayConfigUtil.setRootCertContent(rootCertContent);
          alipayConfigUtil.setRootCertSN(alipayRootCertSN);
  
          //获取应用证书
          if (Strings.isNullOrEmpty(alipayConfig.getAppCertContent()) && Strings.isNullOrEmpty(alipayConfig.getAppCertPath())) {
              throw new ApiException("支付宝根证书路径和根证书内容为空，请填写支付宝根证书路径字段[rootCertPath]或根证书内容字符串字段[rootCertContent]");
          }
          String appCertContent = Strings.isNullOrEmpty(alipayConfig.getAppCertContent()) ?
                  AntCertificationUtil.readCertContent(alipayConfig.getAppCertPath()) : alipayConfig.getAppCertContent();
          String appCertSN = null;
          try {
              //appCertSN为最终发送给网关的应用证书序列号
              appCertSN = AntCertificationUtil.getCertSN(appCertContent);
          } catch (RuntimeException e) {
              AlipayLogger.logBizError(e);
          }
          if (Strings.isNullOrEmpty(appCertSN)) {
              throw new ApiException("AppCert Is Invalid，请检查应用公钥证书路径字段[appCertPath]或应用公钥证书内容字符串字段[appCertContent]对应的应用公钥证书是否正确");
          }
          alipayConfigUtil.setAppCertSN(appCertSN);
  
          //获取支付宝公钥证书
          try {
              String alipayPublicCertContent = Strings.isNullOrEmpty(alipayConfig.getAlipayPublicCertContent()) ?
                      AntCertificationUtil.readCertContent(alipayConfig.getAlipayPublicCertPath()) : alipayConfig.getAlipayPublicCertContent();
              //alipayCertSN为支付宝公钥证书序列号
              String alipayCertSN = AntCertificationUtil.getCertSN(alipayPublicCertContent);
              //获取支付宝公钥以序列号为key存入map
              alipayConfigUtil.getCachedAlipayPublicKey()
                      .put(alipayCertSN, AntCertificationUtil.getCertPublicKey(alipayPublicCertContent));
          } catch (RuntimeException e) {
              AlipayLogger.logBizError(e);
              throw new ApiException("提取支付宝公钥证书失败，请检查支付宝公钥证书路径字段[alipayPublicCertPath]或支付宝公钥证书内容字符串字段[alipayPublicCertContent]对应的支付宝公钥证书是否正确");
          }
      }
  
      private void initHttpClient() {
          initHttpClient(Collections.<Interceptor>emptyList());
      }
  
      private void initHttpClient(List<Interceptor> interceptors) {
          OkHttpClient.Builder builder = new OkHttpClient.Builder();
          builder.addNetworkInterceptor(getProgressInterceptor());
          for (Interceptor interceptor: interceptors) {
              builder.addInterceptor(interceptor);
          }
  
          httpClient = builder.build();
      }
  
      private void init() {
          verifyingSsl = true;
  
          json = new JSON();
  
          // Set default User-Agent.
          setUserAgent("OpenAPI-Generator/3.1.53.ALL/java");
          //Set default sdkVersion
          setSdkVersion("OpenAPI-Generator/3.1.53.ALL/java");
  
          authentications = new HashMap<String, Authentication>();
      }
  
      /**
       * Get base path
       *
       * @return Base path
       */
      public String getBasePath() {
          return basePath;
      }
  
      /**
       * Set base path
       *
       * @param basePath Base path of the URL (e.g https://openapi.alipay.com
       * @return An instance of OkHttpClient
       */
      public ApiClient setBasePath(String basePath) {
          this.basePath = basePath;
          return this;
      }
  
      /**
       * Get HTTP client
       *
       * @return An instance of OkHttpClient
       */
      public OkHttpClient getHttpClient() {
          return httpClient;
      }
  
      /**
       * Set HTTP client, which must never be null.
       *
       * @param newHttpClient An instance of OkHttpClient
       * @return Api Client
       * @throws java.lang.NullPointerException when newHttpClient is null
       */
      public ApiClient setHttpClient(OkHttpClient newHttpClient) {
          this.httpClient = Objects.requireNonNull(newHttpClient, "HttpClient must not be null!");
          return this;
      }
  
      /**
       * Get JSON
       *
       * @return JSON object
       */
      public JSON getJSON() {
          return json;
      }
  
      /**
       * Set JSON
       *
       * @param json JSON object
       * @return Api client
       */
      public ApiClient setJSON(JSON json) {
          this.json = json;
          return this;
      }
  
      /**
       * True if isVerifyingSsl flag is on
       *
       * @return True if isVerifySsl flag is on
       */
      public boolean isVerifyingSsl() {
          return verifyingSsl;
      }
  
      /**
       * Configure whether to verify certificate and hostname when making https requests.
       * Default to true.
       * NOTE: Do NOT set to false in production code, otherwise you would face multiple types of cryptographic attacks.
       *
       * @param verifyingSsl True to verify TLS/SSL connection
       * @return ApiClient
       */
      public ApiClient setVerifyingSsl(boolean verifyingSsl) {
          this.verifyingSsl = verifyingSsl;
          applySslSettings();
          return this;
      }
  
      /**
       * Get SSL CA cert.
       *
       * @return Input stream to the SSL CA cert
       */
      public InputStream getSslCaCert() {
          return sslCaCert;
      }
  
      /**
       * Configure the CA certificate to be trusted when making https requests.
       * Use null to reset to default.
       *
       * @param sslCaCert input stream for SSL CA cert
       * @return ApiClient
       */
      public ApiClient setSslCaCert(InputStream sslCaCert) {
          this.sslCaCert = sslCaCert;
          applySslSettings();
          return this;
      }
  
      /**
       * <p>Getter for the field <code>keyManagers</code>.</p>
       *
       * @return an array of {@link javax.net.ssl.KeyManager} objects
       */
      public KeyManager[] getKeyManagers() {
          return keyManagers;
      }
  
      /**
       * Configure client keys to use for authorization in an SSL session.
       * Use null to reset to default.
       *
       * @param managers The KeyManagers to use
       * @return ApiClient
       */
      public ApiClient setKeyManagers(KeyManager[] managers) {
          this.keyManagers = managers;
          applySslSettings();
          return this;
      }
  
      /**
       * <p>Getter for the field <code>dateFormat</code>.</p>
       *
       * @return a {@link java.text.DateFormat} object
       */
      public DateFormat getDateFormat() {
          return dateFormat;
      }
  
      /**
       * <p>Setter for the field <code>dateFormat</code>.</p>
       *
       * @param dateFormat a {@link java.text.DateFormat} object
       * @return a {@link com.alipay.v3.ApiClient} object
       */
      public ApiClient setDateFormat(DateFormat dateFormat) {
          this.json.setDateFormat(dateFormat);
          return this;
      }
  
      /**
       * <p>Set SqlDateFormat.</p>
       *
       * @param dateFormat a {@link java.text.DateFormat} object
       * @return a {@link com.alipay.v3.ApiClient} object
       */
      public ApiClient setSqlDateFormat(DateFormat dateFormat) {
          this.json.setSqlDateFormat(dateFormat);
          return this;
      }
  
      /**
       * <p>Set OffsetDateTimeFormat.</p>
       *
       * @param dateFormat a {@link java.time.format.DateTimeFormatter} object
       * @return a {@link com.alipay.v3.ApiClient} object
       */
      public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
          this.json.setOffsetDateTimeFormat(dateFormat);
          return this;
      }
  
      /**
       * <p>Set LocalDateFormat.</p>
       *
       * @param dateFormat a {@link java.time.format.DateTimeFormatter} object
       * @return a {@link com.alipay.v3.ApiClient} object
       */
      public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) {
          this.json.setLocalDateFormat(dateFormat);
          return this;
      }
  
      /**
       * <p>Set LenientOnJson.</p>
       *
       * @param lenientOnJson a boolean
       * @return a {@link com.alipay.v3.ApiClient} object
       */
      public ApiClient setLenientOnJson(boolean lenientOnJson) {
          this.json.setLenientOnJson(lenientOnJson);
          return this;
      }
  
      /**
       * Get authentications (key: authentication name, value: authentication).
       *
       * @return Map of authentication objects
       */
      public Map<String, Authentication> getAuthentications() {
          return authentications;
      }
  
      /**
       * Get authentication for the given name.
       *
       * @param authName The authentication name
       * @return The authentication, null if not found
       */
      public Authentication getAuthentication(String authName) {
          return authentications.get(authName);
      }
  
  
      /**
       * Helper method to set username for the first HTTP basic authentication.
       *
       * @param username Username
       */
      public void setUsername(String username) {
          for (Authentication auth : authentications.values()) {
              if (auth instanceof HttpBasicAuth) {
                  ((HttpBasicAuth) auth).setUsername(username);
                  return;
              }
          }
          throw new RuntimeException("No HTTP basic authentication configured!");
      }
  
      /**
       * Helper method to set password for the first HTTP basic authentication.
       *
       * @param password Password
       */
      public void setPassword(String password) {
          for (Authentication auth : authentications.values()) {
              if (auth instanceof HttpBasicAuth) {
                  ((HttpBasicAuth) auth).setPassword(password);
                  return;
              }
          }
          throw new RuntimeException("No HTTP basic authentication configured!");
      }
  
      /**
       * Helper method to set API key value for the first API key authentication.
       *
       * @param apiKey API key
       */
      public void setApiKey(String apiKey) {
          for (Authentication auth : authentications.values()) {
              if (auth instanceof ApiKeyAuth) {
                  ((ApiKeyAuth) auth).setApiKey(apiKey);
                  return;
              }
          }
          throw new RuntimeException("No API key authentication configured!");
      }
  
      /**
       * Helper method to set API key prefix for the first API key authentication.
       *
       * @param apiKeyPrefix API key prefix
       */
      public void setApiKeyPrefix(String apiKeyPrefix) {
          for (Authentication auth : authentications.values()) {
              if (auth instanceof ApiKeyAuth) {
                  ((ApiKeyAuth) auth).setApiKeyPrefix(apiKeyPrefix);
                  return;
              }
          }
          throw new RuntimeException("No API key authentication configured!");
      }
  
      /**
       * Helper method to set access token for the first OAuth2 authentication.
       *
       * @param accessToken Access token
       */
      public void setAccessToken(String accessToken) {
          throw new RuntimeException("No OAuth2 authentication configured!");
      }
  
      /**
       * Set the User-Agent header's value (by adding to the default header map).
       *
       * @param userAgent HTTP request's user agent
       * @return ApiClient
       */
      public ApiClient setUserAgent(String userAgent) {
          addDefaultHeader("User-Agent", userAgent);
          return this;
      }
  
      public ApiClient setSdkVersion(String sdkVersion) {
          addDefaultHeader("alipay-sdk-version", sdkVersion);
          return this;
      }
  
      /**
       * Add a default header.
       *
       * @param key The header's key
       * @param value The header's value
       * @return ApiClient
       */
      public ApiClient addDefaultHeader(String key, String value) {
          defaultHeaderMap.put(key, value);
          return this;
      }
  
      /**
       * Add a default cookie.
       *
       * @param key The cookie's key
       * @param value The cookie's value
       * @return ApiClient
       */
      public ApiClient addDefaultCookie(String key, String value) {
          defaultCookieMap.put(key, value);
          return this;
      }
  
      /**
       * Check that whether debugging is enabled for this API client.
       *
       * @return True if debugging is enabled, false otherwise.
       */
      public boolean isDebugging() {
          return debugging;
      }
  
      /**
       * Enable/disable debugging for this API client.
       *
       * @param debugging To enable (true) or disable (false) debugging
       * @return ApiClient
       */
      public ApiClient setDebugging(boolean debugging) {
          if (debugging != this.debugging) {
              if (debugging) {
                  loggingInterceptor = new HttpLoggingInterceptor();
                  loggingInterceptor.setLevel(Level.BODY);
                  httpClient = httpClient.newBuilder().addInterceptor(loggingInterceptor).build();
              } else {
                  final OkHttpClient.Builder builder = httpClient.newBuilder();
                  builder.interceptors().remove(loggingInterceptor);
                  httpClient = builder.build();
                  loggingInterceptor = null;
              }
          }
          this.debugging = debugging;
          return this;
      }
  
      /**
       * The path of temporary folder used to store downloaded files from endpoints
       * with file response. The default value is <code>null</code>, i.e. using
       * the system's default temporary folder.
       *
       * @see <a href="https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#createTempFile(java.lang.String,%20java.lang.String,%20java.nio.file.attribute.FileAttribute...)">createTempFile</a>
       * @return Temporary folder path
       */
      public String getTempFolderPath() {
          return tempFolderPath;
      }
  
      /**
       * Set the temporary folder path (for downloading files)
       *
       * @param tempFolderPath Temporary folder path
       * @return ApiClient
       */
      public ApiClient setTempFolderPath(String tempFolderPath) {
          this.tempFolderPath = tempFolderPath;
          return this;
      }
  
      /**
       * Get connection timeout (in milliseconds).
       *
       * @return Timeout in milliseconds
       */
      public int getConnectTimeout() {
          return httpClient.connectTimeoutMillis();
      }
  
      /**
       * Sets the connect timeout (in milliseconds).
       * A value of 0 means no timeout, otherwise values must be between 1 and
       * {@link java.lang.Integer#MAX_VALUE}.
       *
       * @param connectionTimeout connection timeout in milliseconds
       * @return Api client
       */
      public ApiClient setConnectTimeout(int connectionTimeout) {
          httpClient = httpClient.newBuilder().connectTimeout(connectionTimeout, TimeUnit.MILLISECONDS).build();
          return this;
      }
  
      /**
       * Get read timeout (in milliseconds).
       *
       * @return Timeout in milliseconds
       */
      public int getReadTimeout() {
          return httpClient.readTimeoutMillis();
      }
  
      /**
       * Sets the read timeout (in milliseconds).
       * A value of 0 means no timeout, otherwise values must be between 1 and
       * {@link java.lang.Integer#MAX_VALUE}.
       *
       * @param readTimeout read timeout in milliseconds
       * @return Api client
       */
      public ApiClient setReadTimeout(int readTimeout) {
          httpClient = httpClient.newBuilder().readTimeout(readTimeout, TimeUnit.MILLISECONDS).build();
          return this;
      }
  
      /**
       * Get write timeout (in milliseconds).
       *
       * @return Timeout in milliseconds
       */
      public int getWriteTimeout() {
          return httpClient.writeTimeoutMillis();
      }
  
      /**
       * Sets the write timeout (in milliseconds).
       * A value of 0 means no timeout, otherwise values must be between 1 and
       * {@link java.lang.Integer#MAX_VALUE}.
       *
       * @param writeTimeout connection timeout in milliseconds
       * @return Api client
       */
      public ApiClient setWriteTimeout(int writeTimeout) {
          httpClient = httpClient.newBuilder().writeTimeout(writeTimeout, TimeUnit.MILLISECONDS).build();
          return this;
      }
  
  
      /**
       * Format the given parameter object into string.
       *
       * @param param Parameter
       * @return String representation of the parameter
       */
      public String parameterToString(Object param) {
          if (param == null) {
              return "";
          } else if (param instanceof Date || param instanceof OffsetDateTime || param instanceof LocalDate) {
              //Serialize to json string and remove the " enclosing characters
              String jsonStr = json.serialize(param);
              return jsonStr.substring(1, jsonStr.length() - 1);
          } else if (param instanceof Collection) {
              StringBuilder b = new StringBuilder();
              for (Object o : (Collection) param) {
                  if (b.length() > 0) {
                      b.append(",");
                  }
                  b.append(String.valueOf(o));
              }
              return b.toString();
          } else if (param instanceof String) {
              return String.valueOf(param);
          } else {
              return json.serialize(param);
          }
      }
  
      /**
       * Formats the specified query parameter to a list containing a single {@code Pair} object.
       *
       * Note that {@code value} must not be a collection.
       *
       * @param name The name of the parameter.
       * @param value The value of the parameter.
       * @return A list containing a single {@code Pair} object.
       */
      public List<Pair> parameterToPair(String name, Object value) {
          List<Pair> params = new ArrayList<Pair>();
  
          // preconditions
          if (name == null || name.isEmpty() || value == null || value instanceof Collection) {
              return params;
          }
  
          params.add(new Pair(name, parameterToString(value)));
          return params;
      }
  
      /**
       * Formats the specified collection query parameters to a list of {@code Pair} objects.
       *
       * Note that the values of each of the returned Pair objects are percent-encoded.
       *
       * @param collectionFormat The collection format of the parameter.
       * @param name The name of the parameter.
       * @param value The value of the parameter.
       * @return A list of {@code Pair} objects.
       */
      public List<Pair> parameterToPairs(String collectionFormat, String name, Collection value) {
          List<Pair> params = new ArrayList<Pair>();
  
          // preconditions
          if (name == null || name.isEmpty() || value == null || value.isEmpty()) {
              return params;
          }
  
          // create the params based on the collection format
          if ("multi".equals(collectionFormat)) {
              for (Object item : value) {
                  params.add(new Pair(name, escapeString(parameterToString(item))));
              }
              return params;
          }
  
          // collectionFormat is assumed to be "csv" by default
          String delimiter = ",";
  
          // escape all delimiters except commas, which are URI reserved
          // characters
          if ("ssv".equals(collectionFormat)) {
              delimiter = escapeString(" ");
          } else if ("tsv".equals(collectionFormat)) {
              delimiter = escapeString("\t");
          } else if ("pipes".equals(collectionFormat)) {
              delimiter = escapeString("|");
          }
  
          StringBuilder sb = new StringBuilder();
          for (Object item : value) {
              sb.append(delimiter);
              sb.append(escapeString(parameterToString(item)));
          }
  
          params.add(new Pair(name, sb.substring(delimiter.length())));
  
          return params;
      }
  
      /**
       * Formats the specified collection path parameter to a string value.
       *
       * @param collectionFormat The collection format of the parameter.
       * @param value The value of the parameter.
       * @return String representation of the parameter
       */
      public String collectionPathParameterToString(String collectionFormat, Collection value) {
          // create the value based on the collection format
          if ("multi".equals(collectionFormat)) {
              // not valid for path params
              return parameterToString(value);
          }
  
          // collectionFormat is assumed to be "csv" by default
          String delimiter = ",";
  
          if ("ssv".equals(collectionFormat)) {
              delimiter = " ";
          } else if ("tsv".equals(collectionFormat)) {
              delimiter = "\t";
          } else if ("pipes".equals(collectionFormat)) {
              delimiter = "|";
          }
  
          StringBuilder sb = new StringBuilder() ;
          for (Object item : value) {
              sb.append(delimiter);
              sb.append(parameterToString(item));
          }
  
          return sb.substring(delimiter.length());
      }
  
      /**
       * Sanitize filename by removing path.
       * e.g. ../../sun.gif becomes sun.gif
       *
       * @param filename The filename to be sanitized
       * @return The sanitized filename
       */
      public String sanitizeFilename(String filename) {
          return filename.replaceAll(".*[/\\\\]", "");
      }
  
      /**
       * Check if the given MIME is a JSON MIME.
       * JSON MIME examples:
       *   application/json
       *   application/json; charset=UTF8
       *   APPLICATION/JSON
       *   application/vnd.company+json
       * "* / *" is also default to JSON
       * @param mime MIME (Multipurpose Internet Mail Extensions)
       * @return True if the given MIME is JSON, false otherwise.
       */
      public boolean isJsonMime(String mime) {
          String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$";
          return mime != null && (mime.matches(jsonMime) || mime.equals("*/*"));
      }
  
      /**
       * Select the Accept header's value from the given accepts array:
       *   if JSON exists in the given array, use it;
       *   otherwise use all of them (joining into a string)
       *
       * @param accepts The accepts array to select from
       * @return The Accept header to use. If the given array is empty,
       *   null will be returned (not to set the Accept header explicitly).
       */
      public String selectHeaderAccept(String[] accepts) {
          if (accepts.length == 0) {
              return null;
          }
          for (String accept : accepts) {
              if (isJsonMime(accept)) {
                  return accept;
              }
          }
          return StringUtil.join(accepts, ",");
      }
  
      /**
       * Select the Content-Type header's value from the given array:
       *   if JSON exists in the given array, use it;
       *   otherwise use the first one of the array.
       *
       * @param contentTypes The Content-Type array to select from
       * @return The Content-Type header to use. If the given array is empty,
       *   returns null. If it matches "any", JSON will be used.
       */
      public String selectHeaderContentType(String[] contentTypes) {
          if (contentTypes.length == 0) {
              return null;
          }
  
          if (contentTypes[0].equals("*/*")) {
              return "application/json";
          }
  
          for (String contentType : contentTypes) {
              if (isJsonMime(contentType)) {
                  return contentType;
              }
          }
  
          return contentTypes[0];
      }
  
      /**
       * Escape the given string to be used as URL query value.
       *
       * @param str String to be escaped
       * @return Escaped string
       */
      public String escapeString(String str) {
          try {
              return URLEncoder.encode(str, "utf8").replaceAll("\\+", "%20");
          } catch (UnsupportedEncodingException e) {
              return str;
          }
      }
  
      /**
       * Deserialize response body to Java object, according to the return type and
       * the Content-Type response header.
       *
       * @param <T> Type
       * @param response HTTP response
       * @param returnType The type of the Java object
       * @return The deserialized Java object
       * @throws com.alipay.v3.ApiException If fail to deserialize response body, i.e. cannot read response body
       *   or the Content-Type of the response is not supported.
       */
      @SuppressWarnings("unchecked")
      public <T> T deserialize(Response response, Type returnType) throws ApiException {
          if (response == null || returnType == null) {
              return null;
          }
  
          if ("byte[]".equals(returnType.toString())) {
              // Handle binary response (byte array).
              try {
                  return (T) response.body().bytes();
              } catch (IOException e) {
                  throw new ApiException(e);
              }
          } else if (returnType.equals(File.class)) {
              // Handle file downloading.
              return (T) downloadFileFromResponse(response);
          }
  
          String respBody;
          try {
              if (response.body() != null)
                  respBody = response.body().string();
              else
                  respBody = null;
          } catch (IOException e) {
              throw new ApiException(e);
          }
  
          if (respBody == null || "".equals(respBody)) {
              return null;
          }
  
          verifyResponse(respBody, response, false);
          //解密
          respBody = alipayConfigUtil.decrypt(respBody);
          AlipayLogger.logBizResponseInfo(response.code(), respBody, response.headers().toMultimap());
  
          String contentType = response.headers().get("Content-Type");
          if (contentType == null) {
              // ensuring a default content type
              contentType = "application/json";
          }
          if (isJsonMime(contentType) || "text/plain".equals(contentType)) {
              return json.deserialize(respBody, returnType);
          } else if (returnType.equals(String.class)) {
              // Expecting string, return the raw response body.
              return (T) respBody;
          } else {
              throw new ApiException(
                      "Content type \"" + contentType + "\" is not supported for type: " + returnType,
                      response.code(),
                      response.headers().toMultimap(),
                      respBody);
          }
      }
  
      /**
       * 验签及解密
       *
       * @param respBody
       * @param response
       * @param isCheckSign true:检查header中sign存在才验签
       * @return
       */
      private void verifyResponse(String respBody, Response response, boolean isCheckSign) throws ApiException {
          //验签
          String sign = response.headers().get("alipay-signature");
          if (isCheckSign && (Strings.isNullOrEmpty(sign) || "null".equals(sign))) {
              return;
          }
          String alipayCertSN = response.headers().get("alipay-sn");
          String timestamp = response.headers().get("alipay-timestamp");
          String nonce = response.headers().get("alipay-nonce");
          boolean verify = alipayConfigUtil.verify(respBody, sign, alipayCertSN, timestamp, nonce);
          if (!verify) {
              throw new ApiException("sign check fail: check Sign and Data Fail!", 400, response.headers().toMultimap(), respBody);
          }
      }
  
      /**
       * Serialize the given Java object into request body according to the object's
       * class and the request Content-Type.
       *
       * @param obj The Java object
       * @param contentType The request Content-Type
       * @return The serialized request body
       * @throws com.alipay.v3.ApiException If fail to serialize the given object
       */
      public RequestBody serialize(Object obj, String contentType) throws ApiException {
          if (obj instanceof byte[]) {
              // Binary (byte array) body parameter support.
              return RequestBody.create((byte[]) obj, MediaType.parse(contentType));
          } else if (obj instanceof File) {
              // File body parameter support.
              return RequestBody.create((File) obj, MediaType.parse(contentType));
          } else if (obj instanceof ByteStreamWrapper) {
              ByteStreamWrapper wrapper = (ByteStreamWrapper) obj;
              return RequestBody.create(wrapper.getContent(), MediaType.parse(contentType));
          } else if ("text/plain".equals(contentType) && obj instanceof String) {
              return RequestBody.create((String) obj, MediaType.parse(contentType));
          } else if (isJsonMime(contentType)) {
              String content = parameterToString(obj);
              return RequestBody.create(content, MediaType.parse(contentType));
          } else {
              throw new ApiException("Content type \"" + contentType + "\" is not supported");
          }
      }
  
      /**
       * Download file from the given response.
       *
       * @param response An instance of the Response object
       * @throws com.alipay.v3.ApiException If fail to read file content from response and write to disk
       * @return Downloaded file
       */
      public File downloadFileFromResponse(Response response) throws ApiException {
          try {
              File file = prepareDownloadFile(response);
              BufferedSink sink = Okio.buffer(Okio.sink(file));
              sink.writeAll(response.body().source());
              sink.close();
              return file;
          } catch (IOException e) {
              throw new ApiException(e);
          }
      }
  
      /**
       * Prepare file for download
       *
       * @param response An instance of the Response object
       * @return Prepared file for the download
       * @throws java.io.IOException If fail to prepare file for download
       */
      public File prepareDownloadFile(Response response) throws IOException {
          String filename = null;
          String contentDisposition = response.header("Content-Disposition");
          if (contentDisposition != null && !"".equals(contentDisposition)) {
              // Get filename from the Content-Disposition header.
              Pattern pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
              Matcher matcher = pattern.matcher(contentDisposition);
              if (matcher.find()) {
                  filename = sanitizeFilename(matcher.group(1));
              }
          }
  
          String prefix = null;
          String suffix = null;
          if (filename == null) {
              prefix = "download-";
              suffix = "";
          } else {
              int pos = filename.lastIndexOf(".");
              if (pos == -1) {
                  prefix = filename + "-";
              } else {
                  prefix = filename.substring(0, pos) + "-";
                  suffix = filename.substring(pos);
              }
              // Files.createTempFile requires the prefix to be at least three characters long
              if (prefix.length() < 3)
                  prefix = "download-";
          }
  
          if (tempFolderPath == null)
              return Files.createTempFile(prefix, suffix).toFile();
          else
              return Files.createTempFile(Paths.get(tempFolderPath), prefix, suffix).toFile();
      }
  
      /**
       * {@link #execute(Call, Type)}
       *
       * @param <T> Type
       * @param call An instance of the Call object
       * @return ApiResponse&lt;T&gt;
       * @throws com.alipay.v3.ApiException If fail to execute the call
       */
      public <T> ApiResponse<T> execute(Call call) throws ApiException {
          return execute(call, null);
      }
  
      /**
       * Execute HTTP call and deserialize the HTTP response body into the given return type.
       *
       * @param returnType The return type used to deserialize HTTP response body
       * @param <T> The return type corresponding to (same with) returnType
       * @param call Call
       * @return ApiResponse object containing response status, headers and
       *   data, which is a Java object deserialized from response body and would be null
       *   when returnType is null.
       * @throws com.alipay.v3.ApiException If fail to execute the call
       */
      public <T> ApiResponse<T> execute(Call call, Type returnType) throws ApiException {
          try {
              Response response = call.execute();
              T data = handleResponse(response, returnType);
              return new ApiResponse<T>(response.code(), response.headers().toMultimap(), data);
          } catch (IOException e) {
              AlipayLogger.logBizError(e);
              throw new ApiException(e);
          } catch (ApiException e) {
              AlipayLogger.logBizError(e);
              throw e;
          } catch (Exception e) {
              AlipayLogger.logBizError(e);
              throw e;
          }
      }
  
      /**
       * {@link #executeAsync(Call, Type, ApiCallback)}
       *
       * @param <T> Type
       * @param call An instance of the Call object
       * @param callback ApiCallback&lt;T&gt;
       */
      public <T> void executeAsync(Call call, ApiCallback<T> callback) {
          executeAsync(call, null, callback);
      }
  
      /**
       * Execute HTTP call asynchronously.
       *
       * @param <T> Type
       * @param call The callback to be executed when the API call finishes
       * @param returnType Return type
       * @param callback ApiCallback
       * @see #execute(Call, Type)
       */
      @SuppressWarnings("unchecked")
      public <T> void executeAsync(Call call, final Type returnType, final ApiCallback<T> callback) {
          call.enqueue(new Callback() {
              @Override
              public void onFailure(Call call, IOException e) {
                  callback.onFailure(new ApiException(e), 0, null);
              }
  
              @Override
              public void onResponse(Call call, Response response) throws IOException {
                  T result;
                  try {
                      result = (T) handleResponse(response, returnType);
                  } catch (ApiException e) {
                      callback.onFailure(e, response.code(), response.headers().toMultimap());
                      return;
                  } catch (Exception e) {
                      callback.onFailure(new ApiException(e), response.code(), response.headers().toMultimap());
                      return;
                  }
                  callback.onSuccess(result, response.code(), response.headers().toMultimap());
              }
          });
      }
  
      /**
       * 流式调用(SSE)
       *
       * @param request  The HTTP request
       * @param callback 回调函数
       * @param <T>      Type
       */
      public <T> void streamCall(Request request, StreamCallback<T> callback) {
          RealEventSource eventSource = new RealEventSource(request, new EventSourceListener() {
              @Override
              public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
                  callback.onOpen(response);
              }
  
              @Override
              public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) {
                  try {
                      ApiResponse<T> apiResponse = new ApiResponse<T>(200, null,
                              ("".equals(data) || callback.getReturnType() == null)
                                      ? null : json.deserialize(data, callback.getReturnType().getType()));
                      callback.onEvent(apiResponse, id, type, data);
                  } catch (Exception e) {
                      AlipayLogger.logBizWarn(e);
                      callback.onEvent(null, id, type, data);
                  }
              }
  
              @Override
              public void onClosed(@NotNull EventSource eventSource) {
                  callback.onClosed();
              }
  
              @Override
              public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
                  if (response == null) {
                      callback.onError(new ApiException(t));
                      return;
                  }
                  try {
                      String respBody = null;
                      String contentType = response.header("Content-Type");
                      if (contentType != null
                              && (contentType.toLowerCase().contains("application/json")) && response.body() != null) {
                          try {
                              respBody = response.body().string();
                          } catch (IOException e) {
                              callback.onError(new ApiException(response.message(), e, response.code(), response.headers().toMultimap()));
                              return;
                          }
                      }
                      ApiException apiException = new ApiException(response.message(), t, response.code(), response.headers().toMultimap(), respBody);
                      callback.onError(apiException);
                  } catch (Exception e) {
                      AlipayLogger.logBizWarn(e);
                      callback.onError(new ApiException(e));
                  }
              }
          });
  
          // 建立连接开始请求
          eventSource.connect(httpClient);
      }
  
      /**
       * Handle the given response, return the deserialized object when the response is successful.
       *
       * @param <T> Type
       * @param response Response
       * @param returnType Return type
       * @return Type
       * @throws com.alipay.v3.ApiException If the response has an unsuccessful status code or
       *                      fail to deserialize the response body
       */
      public <T> T handleResponse(Response response, Type returnType) throws ApiException {
          if (response.isSuccessful()) {
              if (returnType == null || response.code() == 204) {
                  // returning null if the returnType is not defined,
                  // or the status code is 204 (No Content)
                  if (response.body() != null) {
                      try {
                          response.body().close();
                      } catch (Exception e) {
                          throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap());
                      }
                  }
                  AlipayLogger.logBizResponseInfo(response.code(), null, response.headers().toMultimap());
                  return null;
              } else {
                  return deserialize(response, returnType);
              }
          } else {
              String respBody = null;
              if (response.body() != null) {
                  try {
                      respBody = response.body().string();
                      verifyResponse(respBody, response, true);
                  } catch (IOException e) {
                      throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap());
                  }
              }
              throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), respBody);
          }
      }
  
      /**
       * Build HTTP call with the given options.
       *
       * @param baseUrl The base URL
       * @param path The sub-path of the HTTP URL
       * @param method The request method, one of "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH" and "DELETE"
       * @param queryParams The query parameters
       * @param collectionQueryParams The collection query parameters
       * @param body The request body object
       * @param headerParams The header parameters
       * @param cookieParams The cookie parameters
       * @param formParams The form parameters
       * @param authNames The authentications to apply
       * @param callback Callback for upload/download progress
       * @return The HTTP call
       * @throws com.alipay.v3.ApiException If fail to serialize the request body object
       */
      public Call buildCall(String baseUrl, String path, String method, List<Pair> queryParams, List<Pair> collectionQueryParams, Object body, Map<String, String> headerParams, Map<String, String> cookieParams, Map<String, Object> formParams, String[] authNames, ApiCallback callback) throws ApiException {
          Request request = buildRequest(baseUrl, path, method, queryParams, collectionQueryParams, body, headerParams, cookieParams, formParams, authNames, callback);
  
          return httpClient.newCall(request);
      }
  
      /**
       * Build HTTP call with the HTTP request.
       *
       * @param request The HTTP request
       * @return
       */
      public Call buildCall(Request request) {
          return httpClient.newCall(request);
      }
  
      /**
       * Build an HTTP request with the given options.
       *
       * @param baseUrl The base URL
       * @param path The sub-path of the HTTP URL
       * @param method The request method, one of "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH" and "DELETE"
       * @param queryParams The query parameters
       * @param collectionQueryParams The collection query parameters
       * @param body The request body object
       * @param headerParams The header parameters
       * @param cookieParams The cookie parameters
       * @param formParams The form parameters
       * @param authNames The authentications to apply
       * @param callback Callback for upload/download progress
       * @return The HTTP request
       * @throws com.alipay.v3.ApiException If fail to serialize the request body object
       */
      public Request buildRequest(String baseUrl, String path, String method, List<Pair> queryParams, List<Pair> collectionQueryParams, Object body, Map<String, String> headerParams, Map<String, String> cookieParams, Map<String, Object> formParams, String[] authNames, ApiCallback callback) throws ApiException {
          // aggregate queryParams (non-collection) and collectionQueryParams into allQueryParams
          List<Pair> allQueryParams = new ArrayList<Pair>(queryParams);
          allQueryParams.addAll(collectionQueryParams);
  
          final String url = buildUrl(path, queryParams, collectionQueryParams);
          String fullUrl = Strings.isNullOrEmpty(baseUrl) ? basePath + url : baseUrl + url;
  
          String contentType = headerParams.get("Content-Type");
  
          String httpRequestBody = getEncryptRequestBody(body, formParams, contentType, headerParams);
          //加签
          alipayConfigUtil.sign(method, url, httpRequestBody, headerParams);
          //requestId
          if (!headerParams.containsKey("alipay-request-id")) {
              headerParams.put("alipay-request-id", UUID.randomUUID().toString());
          }
  
          // prepare HTTP request body
          RequestBody reqBody;
          contentType = headerParams.get("Content-Type");
          String requestId = headerParams.get("alipay-request-id");
          AlipayLogger.logBizInfo(fullUrl, httpRequestBody, method, contentType, requestId);
  
          if (!HttpMethod.permitsRequestBody(method)) {
              reqBody = null;
          } else if ("application/x-www-form-urlencoded".equals(contentType)) {
              reqBody = buildRequestBodyFormEncoding(formParams);
          } else if ("multipart/form-data".equals(contentType)) {
              reqBody = buildRequestBodyMultipart(formParams, headerParams.containsKey("alipay-encrypt-type"));
          } else if (httpRequestBody == null) {
              if ("DELETE".equals(method)) {
                  // allow calling DELETE without sending a request body
                  reqBody = null;
              } else {
                  // use an empty request body (for POST, PUT and PATCH)
                  reqBody = RequestBody.create("", contentType == null ? null : MediaType.parse(contentType));
              }
          } else {
              reqBody = serialize(httpRequestBody, contentType);
          }
  
          // update parameters with authentication settings
          updateParamsForAuth(authNames, allQueryParams, headerParams, cookieParams, requestBodyToString(reqBody), method, URI.create(url));
  
          final Request.Builder reqBuilder = new Request.Builder().url(fullUrl);
          processHeaderParams(headerParams, reqBuilder);
          processCookieParams(cookieParams, reqBuilder);
  
          // Associate callback with request (if not null) so interceptor can
          // access it when creating ProgressResponseBody
          reqBuilder.tag(callback);
  
          Request request = null;
  
          if (callback != null && reqBody != null) {
              ProgressRequestBody progressRequestBody = new ProgressRequestBody(reqBody, callback);
              request = reqBuilder.method(method, progressRequestBody).build();
          } else {
              request = reqBuilder.method(method, reqBody).build();
          }
  
          return request;
      }
  
      /**
       * 获取加密的body字符串
       *
       * @param body         The request body object
       * @param formParams   Form parameters in the form of Map
       * @param contentType  content type
       * @param headerParams The header parameters
       * @return body字符串
       */
      public String getEncryptRequestBody(Object body, Map<String, Object> formParams, String contentType, Map<String, String> headerParams) throws ApiException {
          String httpRequestBody = "";
          if ("application/x-www-form-urlencoded".equals(contentType)) {
              for (Entry<String, Object> param : formParams.entrySet()) {
                  formParams.put(param.getKey(), parameterToString(param.getValue()));
              }
              httpRequestBody = json.serialize(formParams);
          } else if ("multipart/form-data".equals(contentType)) {
              for (Entry<String, Object> param : formParams.entrySet()) {
                  if (param.getValue() instanceof File || param.getValue() instanceof ByteStreamWrapper) {
                      continue;
                  } else {
                      //加密
                      httpRequestBody = alipayConfigUtil.encrypt(parameterToString(param.getValue()), headerParams);
                      formParams.put(param.getKey(), httpRequestBody);
                      return httpRequestBody;
                  }
              }
              return alipayConfigUtil.encrypt(httpRequestBody, headerParams);
          } else if (body == null) {
              httpRequestBody = null;
          } else if (body instanceof byte[] || body instanceof File || body instanceof ByteStreamWrapper) {
              httpRequestBody = null;
          } else if (body instanceof String) {
              httpRequestBody = String.valueOf(body);
          } else {
              httpRequestBody = json.serialize(body);
          }
          //加密
          return alipayConfigUtil.encrypt(httpRequestBody, headerParams);
      }
  
      /**
       * Build full URL by concatenating the given sub path and query parameters.
       *
       * @param path The sub path
       * @param queryParams The query parameters
       * @param collectionQueryParams The collection query parameters
       * @return The full URL
       */
      public String buildUrl(String path, List<Pair> queryParams, List<Pair> collectionQueryParams) {
          final StringBuilder url = new StringBuilder();
          url.append(path);
  
          if (queryParams != null && !queryParams.isEmpty()) {
              // support (constant) query string in `path`, e.g. "/posts?draft=1"
              String prefix = path.contains("?") ? "&" : "?";
              for (Pair param : queryParams) {
                  if (param.getValue() != null) {
                      if (prefix != null) {
                          url.append(prefix);
                          prefix = null;
                      } else {
                          url.append("&");
                      }
                      String value = parameterToString(param.getValue());
                      url.append(escapeString(param.getName())).append("=").append(escapeString(value));
                  }
              }
          }
  
          if (collectionQueryParams != null && !collectionQueryParams.isEmpty()) {
              String prefix = url.toString().contains("?") ? "&" : "?";
              for (Pair param : collectionQueryParams) {
                  if (param.getValue() != null) {
                      if (prefix != null) {
                          url.append(prefix);
                          prefix = null;
                      } else {
                          url.append("&");
                      }
                      String value = parameterToString(param.getValue());
                      // collection query parameter value already escaped as part of parameterToPairs
                      url.append(escapeString(param.getName())).append("=").append(value);
                  }
              }
          }
  
          return url.toString();
      }
  
      /**
       * Set header parameters to the request builder, including default headers.
       *
       * @param headerParams Header parameters in the form of Map
       * @param reqBuilder Request.Builder
       */
      public void processHeaderParams(Map<String, String> headerParams, Request.Builder reqBuilder) {
          for (Entry<String, String> param : headerParams.entrySet()) {
              reqBuilder.header(param.getKey(), parameterToString(param.getValue()));
          }
          for (Entry<String, String> header : defaultHeaderMap.entrySet()) {
              if (!headerParams.containsKey(header.getKey())) {
                  reqBuilder.header(header.getKey(), parameterToString(header.getValue()));
              }
          }
      }
  
      /**
       * Set cookie parameters to the request builder, including default cookies.
       *
       * @param cookieParams Cookie parameters in the form of Map
       * @param reqBuilder Request.Builder
       */
      public void processCookieParams(Map<String, String> cookieParams, Request.Builder reqBuilder) {
          for (Entry<String, String> param : cookieParams.entrySet()) {
              reqBuilder.addHeader("Cookie", String.format("%s=%s", param.getKey(), param.getValue()));
          }
          for (Entry<String, String> param : defaultCookieMap.entrySet()) {
              if (!cookieParams.containsKey(param.getKey())) {
                  reqBuilder.addHeader("Cookie", String.format("%s=%s", param.getKey(), param.getValue()));
              }
          }
      }
  
      /**
       * Update query and header parameters based on authentication settings.
       *
       * @param authNames The authentications to apply
       * @param queryParams List of query parameters
       * @param headerParams Map of header parameters
       * @param cookieParams Map of cookie parameters
       * @param payload HTTP request body
       * @param method HTTP method
       * @param uri URI
       * @throws com.alipay.v3.ApiException If fails to update the parameters
       */
      public void updateParamsForAuth(String[] authNames, List<Pair> queryParams, Map<String, String> headerParams,
                                      Map<String, String> cookieParams, String payload, String method, URI uri) throws ApiException {
          for (String authName : authNames) {
              Authentication auth = authentications.get(authName);
              if (auth == null) {
                  throw new RuntimeException("Authentication undefined: " + authName);
              }
              auth.applyToParams(queryParams, headerParams, cookieParams, payload, method, uri);
          }
      }
  
      /**
       * Build a form-encoding request body with the given form parameters.
       *
       * @param formParams Form parameters in the form of Map
       * @return RequestBody
       */
      public RequestBody buildRequestBodyFormEncoding(Map<String, Object> formParams) {
          okhttp3.FormBody.Builder formBuilder = new okhttp3.FormBody.Builder();
          for (Entry<String, Object> param : formParams.entrySet()) {
              formBuilder.add(param.getKey(), parameterToString(param.getValue()));
          }
          return formBuilder.build();
      }
  
      /**
       * Build a multipart (file uploading) request body with the given form parameters,
       * which could contain text fields and file fields.
       *
       * @param formParams Form parameters in the form of Map
       * @param isEncrypt 是否加密
       * @return RequestBody
       */
      public RequestBody buildRequestBodyMultipart(Map<String, Object> formParams, boolean isEncrypt) {
          MultipartBody.Builder mpBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
          for (Entry<String, Object> param : formParams.entrySet()) {
              if (param.getValue() instanceof File) {
                  File file = (File) param.getValue();
                  addPartToMultiPartBuilder(mpBuilder, param.getKey(), file);
              } else if (param.getValue() instanceof ByteStreamWrapper) {
                  ByteStreamWrapper wrapper = (ByteStreamWrapper) param.getValue();
                  addPartToMultiPartBuilder(mpBuilder, param.getKey(), wrapper);
              } else if (param.getValue() instanceof List) {
                  List list = (List) param.getValue();
                  for (Object item : list) {
                      if (item instanceof File) {
                          addPartToMultiPartBuilder(mpBuilder, param.getKey(), (File) item);
                      } else if (param.getValue() instanceof ByteStreamWrapper) {
                          addPartToMultiPartBuilder(mpBuilder, param.getKey(), (ByteStreamWrapper) param.getValue());
                      }
                  }
              } else {
                  Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"");
                  mpBuilder.addPart(partHeaders, RequestBody.create(parameterToString(param.getValue()), isEncrypt ? MediaType.parse("text/plain") : MediaType.parse("application/json")));
              }
          }
          return mpBuilder.build();
      }
  
      /**
       * Guess Content-Type header from the given file (defaults to "application/octet-stream").
       *
       * @param file The given file
       * @return The guessed Content-Type
       */
      public String guessContentTypeFromFile(File file) {
          String contentType = URLConnection.guessContentTypeFromName(file.getName());
          if (contentType == null) {
              return "application/octet-stream";
          } else {
              return contentType;
          }
      }
  
      /**
       * Add a Content-Disposition Header for the given key and file to the MultipartBody Builder.
       *
       * @param mpBuilder MultipartBody.Builder
       * @param key The key of the Header element
       * @param file The file to add to the Header
       */
      private void addPartToMultiPartBuilder(MultipartBody.Builder mpBuilder, String key, File file) {
          MediaType mediaType = MediaType.parse(guessContentTypeFromFile(file));
          mpBuilder.addFormDataPart(key, file.getName(), RequestBody.create(file, mediaType));
      }
  
      /**
       * Add a Content-Disposition Header for the given key and byte content to the MultipartBody Builder.
       *
       * @param mpBuilder MultipartBody.Builder
       * @param key       The key of the Header element
       * @param wrapper   The byte content to add to the Header
       */
      private void addPartToMultiPartBuilder(MultipartBody.Builder mpBuilder, String key, ByteStreamWrapper wrapper) {
          MediaType mediaType = MediaType.parse(wrapper.getMimeType());
          mpBuilder.addFormDataPart(key, wrapper.getFileName(), RequestBody.create(wrapper.getContent(), mediaType));
      }
  
      /**
       * 采集并排序所有参数
       *
       * @param method       方法名
       * @param appParams    业务参数
       * @param systemParams 系统参数
       * @return
       * @throws ApiException
       */
      public Map<String, String> getSortedMap(String method, Map<String, String> appParams, Map<String, String> systemParams) throws ApiException {
          systemParams.put("method", method);
          systemParams.put("app_id", alipayConfigUtil.getAppId());
          systemParams.put("timestamp", getTimestamp());
          systemParams.put("format", "json");
          systemParams.put("version", "1.0");
          systemParams.put("alipay_sdk", defaultHeaderMap.get("alipay-sdk-version"));
          systemParams.put("charset", "UTF-8");
          systemParams.put("sign_type", "RSA2");
          systemParams.put("app_cert_sn", alipayConfigUtil.getAppCertSN());
          systemParams.put("alipay_root_cert_sn", alipayConfigUtil.getRootCertSN());
          if (!Strings.isNullOrEmpty(alipayConfigUtil.getEncryptKey())) {
              systemParams.put("encrypt_type", alipayConfigUtil.getEncryptType());
              //加密
              String encryptContent = alipayConfigUtil.encrypt(appParams.get("biz_content"), new HashMap<String, String>());
              appParams.put("biz_content", encryptContent);
          }
          //加签
          Map<String, String> sortedMap = new TreeMap<>(systemParams);
          sortedMap.putAll(appParams);
          StringBuilder content = new StringBuilder();
          int index = 0;
          for (Entry<String, String> pair : sortedMap.entrySet()) {
              if (!Strings.isNullOrEmpty(pair.getKey()) && !Strings.isNullOrEmpty(pair.getValue())) {
                  content.append(index == 0 ? "" : "&").append(pair.getKey()).append("=").append(pair.getValue());
                  index++;
              }
          }
          if (!Strings.isNullOrEmpty(alipayConfigUtil.getPrivateKey())) {
              sortedMap.put("sign", alipayConfigUtil.generateSign(content.toString()));
              systemParams.put("sign", sortedMap.get("sign"));
          }
          return sortedMap;
      }
  
      /**
       * 拼接url
       *
       * @param params    参数
       * @param serverUrl 请求地址
       * @param isNeedUrl 是否需要拼接请求地址
       * @return
       */
      public String buildQuery(Map<String, String> params, String serverUrl, boolean isNeedUrl) {
          String url = null;
          if (isNeedUrl) {
              url = Strings.isNullOrEmpty(serverUrl) ? basePath + "/gateway.do" : serverUrl;
          }
          if (params == null || params.isEmpty()) {
              return url;
          }
          StringBuilder query = new StringBuilder();
          if (isNeedUrl) {
              query.append(url);
              query.append("?");
          }
          boolean hasParam = false;
          for (Entry<String, String> entry : params.entrySet()) {
              String name = entry.getKey();
              String value = entry.getValue();
              // 忽略参数名或参数值为空的参数
              if (!Strings.isNullOrEmpty(name) && !Strings.isNullOrEmpty(value)) {
                  if (hasParam) {
                      query.append("&");
                  } else {
                      hasParam = true;
                  }
  
                  query.append(name).append("=").append(encodeString(value));
              }
          }
  
          return query.toString();
      }
  
      private String encodeString(String str) {
          try {
              return URLEncoder.encode(str, "utf8");
          } catch (UnsupportedEncodingException e) {
              return str;
          }
      }
  
      private String getTimestamp() {
          DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          df.setTimeZone(TimeZone.getTimeZone("GMT+8"));
          return df.format(new Date(System.currentTimeMillis()));
      }
  
      /**
       * Get network interceptor to add it to the httpClient to track download progress for
       * async requests.
       */
      private Interceptor getProgressInterceptor() {
          return new Interceptor() {
              @Override
              public Response intercept(Interceptor.Chain chain) throws IOException {
                  final Request request = chain.request();
                  final Response originalResponse = chain.proceed(request);
                  if (request.tag() instanceof ApiCallback) {
                      final ApiCallback callback = (ApiCallback) request.tag();
                      return originalResponse.newBuilder()
                          .body(new ProgressResponseBody(originalResponse.body(), callback))
                          .build();
                  }
                  return originalResponse;
              }
          };
      }
  
      /**
       * Apply SSL related settings to httpClient according to the current values of
       * verifyingSsl and sslCaCert.
       */
      private void applySslSettings() {
          try {
              TrustManager[] trustManagers;
              HostnameVerifier hostnameVerifier;
              if (!verifyingSsl) {
                  trustManagers = new TrustManager[]{
                          new X509TrustManager() {
                              @Override
                              public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                              }
  
                              @Override
                              public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                              }
  
                              @Override
                              public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                                  return new java.security.cert.X509Certificate[]{};
                              }
                          }
                  };
                  hostnameVerifier = new HostnameVerifier() {
                      @Override
                      public boolean verify(String hostname, SSLSession session) {
                          return true;
                      }
                  };
              } else {
                  TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  
                  if (sslCaCert == null) {
                      trustManagerFactory.init((KeyStore) null);
                  } else {
                      char[] password = null; // Any password will work.
                      CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                      Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(sslCaCert);
                      if (certificates.isEmpty()) {
                          throw new IllegalArgumentException("expected non-empty set of trusted certificates");
                      }
                      KeyStore caKeyStore = newEmptyKeyStore(password);
                      int index = 0;
                      for (Certificate certificate : certificates) {
                          String certificateAlias = "ca" + Integer.toString(index++);
                          caKeyStore.setCertificateEntry(certificateAlias, certificate);
                      }
                      trustManagerFactory.init(caKeyStore);
                  }
                  trustManagers = trustManagerFactory.getTrustManagers();
                  hostnameVerifier = OkHostnameVerifier.INSTANCE;
              }
  
              SSLContext sslContext = SSLContext.getInstance("TLS");
              sslContext.init(keyManagers, trustManagers, new SecureRandom());
              httpClient = httpClient.newBuilder()
                              .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
                              .hostnameVerifier(hostnameVerifier)
                              .build();
          } catch (GeneralSecurityException e) {
              throw new RuntimeException(e);
          }
      }
  
      private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
          try {
              KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
              keyStore.load(null, password);
              return keyStore;
          } catch (IOException e) {
              throw new AssertionError(e);
          }
      }
  
      /**
       * Convert the HTTP request body to a string.
       *
       * @param requestBody The HTTP request body object
       * @return The string representation of the HTTP request body
       */
      private String requestBodyToString(RequestBody requestBody) {
          if (requestBody != null) {
              try {
                  final Buffer buffer = new Buffer();
                  requestBody.writeTo(buffer);
                  return buffer.readUtf8();
              } catch (final IOException e) {
              }
          }
  
          // empty http request body
          return "";
      }
  }
  