# 美团企业版签名实例

# 1 参考示例

假设美团企业版分配的秘钥信息如下(真实秘钥数据以实际分配为准):

名称 取值 备注
entId 617 美团企业版分配的企业ID
accessKey AB4O3CP2R3ER-TK 请求体中需要传此值
secretKey 2Ce0eqKh/ug5+pNnnKAsWA== 参照2.3中AES加密使用

# 2 加密说明

以查询订单详情接口为例,加密流程图如下: 加密流程图

# 2.1 请求体

content为请求体加密内容,具体请求体内容参照各个接口的请求体说明。

名称 类型 是否必填 说明 实例
accessKey String 美团企业版分配给客户的接入秘钥 AB4O3CP2R3ER-TK
content String 请求体内容,将请求参数JSON序列化后进行加密的结果值,加密秘钥使用secretKey参数 O38ebrt4UfPIF8hZOE0LC4S7OZmI_f-YmO1qUi8g4hrbE6Gx8DviEwOGXvCI7Wko

# 2.2 请求体中加密content对应接口的明文请求参数

查询请求:orderDetailQueryRequest

OrderDetailQueryRequest orderDetailQueryRequest = new OrderDetailQueryRequest();
orderDetailQueryRequest.setEntId(617L);
orderDetailQueryRequest.setTs(1512963578L);
orderDetailQueryRequest.setSqtBizOrderId(1L);

# 2.3 待加密的json串明文

plainText = JsonUtil.object2json(orderDetailQueryReqest)

json串明文如下:

{
  	"ts":1512963578,
  	"entId":617,
  	"sqtBizOrderId":1
}

# 2.4 请求参数明文通过AES加密,得到请求体中的加密content

// 加密(此处只针对Java版本调用,其他语言参考第四章各语言加解密参考)
// EncryptUtil类具体内容参照下面第4项的具体内容
// 根据上面所给数据,加密结果为:O38ebrt4UfPIF8hZOE0LC4S7OZmI_f-YmO1qUi8g4hrbE6Gx8DviEwOGXvCI7Wko
content = EncryptUtil.aesEncrypt(plainText, secretKey)

# 3 解密说明

解密流程图如下: 解密流程图

# 3.1 响应参数

data为响应参数加密后的内容,data加密前的数据结构参考各接口规范说明。

名称 类型 说明 实例
traceId String 日志查询ID 56a0af18ae30a168d4006c7c
status Integer 0: 调用成功 其他值均为:调用失败 0
msg String 失败时的错误描述
data String 响应数据,将响应参数JSON序列化后进行加密的结果值,解密秘钥使用secretKey参数 O38ebrt4UfPIF8hZOE0LC4S7OZmI_f-YmO1qUi8g4hrbE6Gx8DviEwOGXvCI7Wko

# 3.2 响应参数通过AES解密,得到data加密前的数据

// 解密(此处只针对Java版本调用,其他语言参考第四章各语言加解密参考)
// EncryptUtil类具体内容参照下面第4项的具体内容
plainText = EncryptUtil.aesDecrypt(data, secretKey)

# 4 各语言加密解密参考

# 4.1 Java加密解密参考

// Base64依赖的pom文件如下
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class EncryptUtil {
    private EncryptUtil() {

    }
    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptUtil.class);
    private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1";
    private static final String BASE_NUMBER = "0123456789";

    // AES加密
    public static String aesEncrypt(String originText, String secret) throws Exception {
        AesCypher cypher = new AesCypher(secret);
        return cypher.encrypt(originText);
    }

    // AES解密
    public static String aesDecrypt(String encryptedText, String secret) throws Exception{
        AesCypher cypher = new AesCypher(secret);
        return cypher.decrypt(encryptedText);
    }

    // 内部aes类
    static class AesCypher {
        private static final Logger LOGGER = LoggerFactory.getLogger(AesCypher.class);

        private static String DEFAULT_SECRET = "2Ce0eqKh/ug5+pNnnKAsWA==";
        private byte[] linebreak;
        private SecretKey key;
        private Cipher cipher;
        private Base64 coder;

        public AesCypher(String secret) {
            this.linebreak = new byte[0];

            try {
                this.coder = new Base64(32, this.linebreak, true);
                byte[] secrets = this.coder.decode(secret);
                // 转换为AES专用密钥
                this.key = new SecretKeySpec(secrets, "AES");
                // 创建密码器,算法/工作模式/补码方式 提供商
                this.cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");
            } catch (Exception e) {
                LOGGER.error("AesCypher.genKey NoSuchAlgorithmException", e);
            }

        }

        public AesCypher() {
            this(DEFAULT_SECRET);
        }

        public synchronized String encrypt(String plainText) throws Exception {
            this.cipher.init(Cipher.ENCRYPT_MODE, this.key);
            byte[] cipherText = this.cipher.doFinal(plainText.getBytes("UTF-8"));
            return new String(this.coder.encode(cipherText));
        }

        public synchronized String decrypt(String codedText) throws Exception {
            byte[] encypted = this.coder.decode(codedText.getBytes());
            this.cipher.init(Cipher.DECRYPT_MODE, this.key);
            byte[] decrypted = this.cipher.doFinal(encypted);
            return new String(decrypted, "UTF-8");
        }
    }
}

# 4.2 Node.js加密解密参考

const crypto = require('crypto');
    
    var SqtAESUtil = {};
    SqtAESUtil.MyConstanst = {
        ENCODING: 'utf8',
        BASE64: 'base64',
        MODE: 'aes-128-ecb',
        IV: new Buffer(''),
        BUFFER: 'buffer',
        SECRETKEY: 'xd1nzb/N9Nx3+VoImzCsnw=='  //美团企业版提供的测试密钥
    };
    
    
    /**
     * 加密
     * @param plainText  要加密的明文内容
     * @returns {string} 返回字符串
     */
    SqtAESUtil.aesEncrypt = function (plainText) {
        var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
    
        var cipherChunks = [];
        var cipher = crypto.createCipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
        cipher.setAutoPadding(true);
    
        cipherChunks.push(cipher.update(new Buffer(plainText, SqtAESUtil.MyConstanst.ENCODING), SqtAESUtil.MyConstanst.BUFFER, SqtAESUtil.MyConstanst.BASE64));
        cipherChunks.push(cipher.final(SqtAESUtil.MyConstanst.BASE64));
    
        return cipherChunks.join('');
    }
    
    /**
     *  解密
     * @param encryptText 密文
     * @returns {string} 字符串
     */
    SqtAESUtil.aesDecrypt = function (encryptText) {
        var secretkey = new Buffer(SqtAESUtil.MyConstanst.SECRETKEY, SqtAESUtil.MyConstanst.BASE64);
    
        var cipherChunks = [];
        var decipher = crypto.createDecipheriv(SqtAESUtil.MyConstanst.MODE, secretkey, SqtAESUtil.MyConstanst.IV);
        decipher.setAutoPadding(true);
    
        cipherChunks.push(decipher.update(encryptText, SqtAESUtil.MyConstanst.BASE64, SqtAESUtil.MyConstanst.ENCODING));
        cipherChunks.push(decipher.final(SqtAESUtil.MyConstanst.ENCODING));
    
        return cipherChunks.join('');
    }
    
    /**
     * 测试加密解密
     */
    SqtAESUtil.test = function () {
        var data = '{"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}';
        var result = SqtAESUtil.aesEncrypt(data);
    
        console.log("加密之前明文内容: " + data);
        console.log("nodeJs加密结果: " + result);
        console.log("nodeJs解密结果: " + SqtAESUtil.aesDecrypt(result));
    
    
        var sqt = 'UgJn07uNgW7S7fJK0R0xVbaLxoCGPQIzoP-_K4Hmp4RduGszhm2mbUs2toZhCtXKP5JGXVTZ9kGts2Wx3IJQCd90ptMoJTDB0vu7mkedEr4KZCvZn77EZLssMC5SpXilmQ-5RXHzvMIT0ASH-IXepTP_O16U37QqCkEb5L1WLy4';
        console.log("美团企业版java生成的密文: " + sqt);
        console.log("nodejs解密: " + SqtAESUtil.aesDecrypt(sqt));
    }
    
    // 测试
    SqtAESUtil.test();

# 4.3 C#加密解密&HTTP调用参考

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class EncryptUtil
{
	#region AES加密
    /// <summary>
    /// AES加密
    /// </summary>
    /// <param name="text">明文</param>
    /// <param name="key">密钥,长度为16的字符串</param>
    /// <returns>密文</returns>
    public static string AESEncode(string text, string key)
    {
        byte[] keys = Convert.FromBase64String(key);
        RijndaelManaged rijndaelCipher = new RijndaelManaged();
        rijndaelCipher.Mode = CipherMode.ECB;
        rijndaelCipher.Padding = PaddingMode.PKCS7;
        rijndaelCipher.KeySize = 128;
        rijndaelCipher.BlockSize = 128;
        byte[] pwdBytes = keys;
        byte[] keyBytes = new byte[16];
        int len = pwdBytes.Length;
        if (len > keyBytes.Length)
            len = keyBytes.Length;
        Array.Copy(pwdBytes, keyBytes, len);
        rijndaelCipher.Key = keyBytes;
        ICryptoTransform transform = rijndaelCipher.CreateEncryptor();
        byte[] plainText = Encoding.UTF8.GetBytes(text);
        byte[] cipherBytes = transform.TransformFinalBlock(plainText, 0, plainText.Length);
        return ConvertHelper.ToBase64StringURLSafe(cipherBytes);//输出为Base64
    }
    #endregion

    #region AES解密
    /// <summary>
    /// AES解密
    /// </summary>
    /// <param name="text">密文</param>
    /// <param name="key">密钥,长度为16的字符串</param>
    /// <returns>明文</returns>
    public static string AESDecode(string text, string key)
    {
        RijndaelManaged rijndaelCipher = new RijndaelManaged();
        rijndaelCipher.Mode = CipherMode.ECB;
        rijndaelCipher.Padding = PaddingMode.PKCS7;
        rijndaelCipher.KeySize = 128;
        rijndaelCipher.BlockSize = 128;
        byte[] encryptedData = ConvertHelper.FromBase64StringURLSafe(text);
        byte[] pwdBytes = Convert.FromBase64String(key);
        byte[] keyBytes = new byte[16];
        int len = pwdBytes.Length;
        if (len > keyBytes.Length)
            len = keyBytes.Length;
        Array.Copy(pwdBytes, keyBytes, len);
        rijndaelCipher.Key = keyBytes;
        ICryptoTransform transform = rijndaelCipher.CreateDecryptor();
        byte[] plainText = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
        return Encoding.UTF8.GetString(plainText);
    }
    #endregion
}
/// C# HTTP调用代码实例参照
var handler = new HttpClientHandler();
handler.AllowAutoRedirect = false;
var httpClient = new HttpClient(handler);
var dict = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("accessKey", ApiConfigManager.MeiTuanTakeoutAccessKey),
    new KeyValuePair<string, string>("content", loginRequestBody)
};
var formUrlEncodedContent = new FormUrlEncodedContent(dict);
formUrlEncodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
formUrlEncodedContent.Headers.ContentType.CharSet = "utf-8";
var response = await httpClient.PostAsync(LOGIN_URL_TEST, formUrlEncodedContent);
if (response.StatusCode == HttpStatusCode.Redirect)
{
    return response.Headers.Location.ToString();
}

依赖的ConvertHelper如下:

/// 美团企业版基于Java实现,标准的加解密格式是urlSafe的Base64编码,该格式会在基础Base64编码的之上,额外处理'+'、'/'、'='
public class ConvertHelper
{
    /// <summary>
    /// 将Java安全的base64字符串转换为byte数组
    /// </summary>
    /// <param name="convert"></param>
    /// <param name="javaURLSafeString"></param>
    /// <returns></returns>
    public static byte[] FromBase64StringURLSafe(string javaURLSafeString)
    {
        javaURLSafeString = javaURLSafeString.Replace("-", "+").Replace("_", "/");
        var base64 = Encoding.ASCII.GetBytes(javaURLSafeString);
        var padding = base64.Length * 3 % 4;
        if (padding != 0)
        {
            javaURLSafeString = javaURLSafeString.PadRight(javaURLSafeString.Length + padding, '=');
        }
        return Convert.FromBase64String(javaURLSafeString);
    }

    /// <summary>
    /// 将byte数组转换为java安全的base64字符串
    /// </summary>
    /// <param name="convert"></param>
    /// <param name="bytes"></param>
    /// <returns></returns>
    public static string ToBase64StringURLSafe(byte[] bytes)
    {
        string base64String = Convert.ToBase64String(bytes);
        return base64String.Replace("+", "-")
            .Replace("/", "_")
            .Replace("=", "");
    }
}

# 4.4 GO语言加密解密参考

package main

import (
	"bytes"
	"crypto/aes"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
)

func main() {
	keys, err := base64.StdEncoding.DecodeString("xd1nzb/N9Nx3+VoImzCsnw==")
	if err != nil {
		fmt.Println(err)
		return
	}
	plainText := `{"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}`

	//aes encryption
	cipherText, err := AESEcbEncrypt(plainText, keys)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("cipherText:", cipherText)

	// aes decryption
	plainText, err = AESEcbDecrypt(cipherText, keys)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("plainText", plainText)

}

//aes/ecb/pkcs7
func AESEcbEncrypt(plainText string, key []byte) (cipherText string, err error) {
	if len(key) != 16 && len(key) != 24 && len(key) != 32 {
		return "", errors.New("crypto/aes: invalid key size " + strconv.Itoa(len(key)))
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return
	}
	plainData := pkcs7Padding([]byte(plainText), block.BlockSize())
	if plainData == nil {
		err = errors.New("unsupported content to be encrypted")
		return
	}
	decrypted := make([]byte, len(plainData))
	size := block.BlockSize()

	for bs, be := 0, size; bs < len(plainData); bs, be = bs+size, be+size {
		block.Encrypt(decrypted[bs:be], plainData[bs:be])
	}
	//urlSafe
	cipherText = base64.RawURLEncoding.EncodeToString(decrypted)
	return
}

//aes/ecb/pkcs7
func AESEcbDecrypt(cipherText string, key []byte) (plainText string, err error) {
	cipherData, err := base64.RawURLEncoding.DecodeString(cipherText)
	if err != nil {
		return
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return
	}
	if len(cipherData)%block.BlockSize() != 0 {
		err = errors.New("cipher text is not a multiple of the block size")
		return
	}
	size := block.BlockSize()
	decrypted := make([]byte, len(cipherData))
	for bs, be := 0, size; bs < len(cipherData); bs, be = bs+size, be+size {
		block.Decrypt(decrypted[bs:be], cipherData[bs:be])
	}
	plainData := pkcsUnPadding(decrypted)
	return string(plainData), nil
}

// The blockSize argument should be 16, 24, or 32.
// Corresponding AES-128, AES-192, or AES-256.
func pkcs7Padding(plainText []byte, blockSize int) []byte {
	paddingSize := blockSize - len(plainText)%blockSize
	paddingText := bytes.Repeat([]byte{byte(paddingSize)}, paddingSize)
	return append(plainText, paddingText...)
}

func pkcsUnPadding(plainText []byte) []byte {
	length := len(plainText)
	number := int(plainText[length-1])
	return plainText[:length-number]
}

# 4.5 PHP语言加密解密参考(以免登为例)

<?php
function aes_encrypt(array $data, $secretKey)
{
    // echo json_encode($data,JSON_UNESCAPED_UNICODE);
    $data = openssl_encrypt(json_encode($data,JSON_UNESCAPED_UNICODE), 'AES-128-ECB', base64_decode($secretKey));
    //      var_dump($data);
    $data = str_replace('/', '_', $data);
    $data = str_replace('+', '-', $data);
    $data = str_replace('=', '', $data);
    //        dd($data);
    return $data;
}

function aes_decrypt(string $str, $secretKey)
    {
        $str = str_replace('_', '/', $str);
        $str = str_replace('-', '+', $str);
        $data = openssl_decrypt($str, 'AES-128-ECB', base64_decode($secretKey));
        //        dd($data);
        // var_dump($data);
        return json_decode($data, TRUE);
    }

function loginFree2Post($url, $data)
{
    // $data=json_encode($data,JSON_UNESCAPED_UNICODE);
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_HEADER, TRUE);
    curl_setopt($curl, CURLOPT_TIMEOUT, 1000);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
    'Accept: application/json',
));//重点
    curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
    $response = curl_exec($curl);
    if (curl_errno($curl)) {
        return curl_error($curl);
    }

    return $response;
}

function getMillisecond() {
    $t = explode(' ', microtime());
    list($s1, $s2) = explode(' ', microtime());
    return (float)sprintf('%.0f',(floatval($s1) + floatval($s2)) * 1000);
}
function randstr($length = 16) {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    $str = "";
    for ($i = 0; $i < $length; $i++) {
        $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
    }
    return $str;
}
//美团外卖入口
function mt_waimai($params)
{
    $url = 'https://waimai-openapi.apigw.test.meituan.com/api/sqt/open/login/h5/loginFree/redirection?test_open_swimlane=test-open';
    $staffPhone = isset($params['mobile'])?$params['mobile']:''; //员工手机号 1. 登录时, staffPhone/staffEmail/staffNum 三者必填一个, 与企业员工唯一识别对应
    $staffEmail = isset($params['staffEmail'])?$params['staffEmail']:''; //员工邮箱
    $staffNum = isset($params['staffNum'])?$params['staffNum']:''; //员工工号
    $externalOrgId = isset($params['externalOrgId'])?$params['externalOrgId']:''; //部门唯一标识
    $orderId = isset($params['orderId'])?$params['orderId']:''; //唯一订单号

    $ts = getMillisecond();
    $staffInfo = ['staffPhone'=> $staffPhone];
    $nonce = randstr(32);

    $longitude = isset($params['longitude'])?$params['longitude']:''; //经度 116.480881
    $latitude = isset($params['latitude'])?$params['latitude']:''; //纬度 39.989410
    $geotype = isset($params['geotype'])?$params['geotype']:'wgs84'; //gcj02(火星坐标系)或者wgs84(国际坐标系)
    $address = isset($params['address'])?$params['address']:''; //经纬度对应的中文地址北京市朝阳区阜通东大街6号
    $location = ['longitude' => $longitude, 'latitude' => $latitude, 'geotype' => $geotype, 'address' => $address];
    $bizParam = ['location' => $location];
    $bizParam = [];
    $data = ['productType' => 'mt_waimai', 'ts'=>$ts, 'entId' => '103393', 'staffInfo' => $staffInfo, 'nonce' => $nonce,];
    $content = aes_encrypt($data, '+AN4Qre9BaJsmPQBSzEXGA==');
    $postData = ['accessKey' => 'CPQ3H92TTWQQ-TK', 'content' => $content];
    $result = loginFree2Post($url, $postData);
    return $result;
}

# 4.6 Python语言加密解密参考

from Crypto.Cipher import AES
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode

BLOCK_SIZE = AES.block_size

class AESCipher:

    def __init__(self, key):
        self.key = b64decode(key)

    @staticmethod
    def pad(text):
        return text + (BLOCK_SIZE - len(text.encode()) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(text.encode()) % BLOCK_SIZE)

    @staticmethod
    def un_pad(text):
        return text[:-ord(text[len(text) - 1:])]

    def encrypt(self, text):
        """
        加密
        """
        text = self.pad(text).encode()
        cipher = AES.new(key=self.key, mode=AES.MODE_ECB)
        encrypted_text = cipher.encrypt(text)
        return urlsafe_b64encode(encrypted_text).decode('utf-8')

    def decrypt(self, encrypted_text):
        """
        解密
        """
        encrypted_text = pad_content(encrypted_text)
        encrypted_text = urlsafe_b64decode(encrypted_text)
        cipher = AES.new(key=self.key, mode=AES.MODE_ECB)
        decrypted_text = cipher.decrypt(encrypted_text)
        return self.un_pad(decrypted_text).decode('utf-8')
    def pad_content(content):
        mod_result = len(content) % 4
        if mod_result > 0:
            padding_size = 4 - mod_result
            content += '=' * padding_size
        return content

if __name__ == '__main__':
    # 美团企业版secretKey
    secret = 'GFfzmJDuV88zYFvCBHML6g=='
    cipher = AESCipher(key=secret)
    # 请求体
    plain_text = '{"sign":"sgW1bxc7oatFhOJXAeHnNg==","ts":1512964057,"method":"waimai.poi.list","longitude":116488645,"latitude":40007069}'

    # 加密
    cipherText = cipher.encrypt(plain_text)
    print(cipherText)

    # 解密
    plain_text = cipher.decrypt(cipherText)
    print(plain_text)

# 5 完整调用实例

# 5.1 SqtClient类(main方法入口开始)

import com.meituan.sqt.request.BaseApiRequest;
import com.meituan.sqt.request.OrderDetailQueryReq;
import com.meituan.sqt.utils.EncryptUtil;
import com.meituan.sqt.utils.HttpClientUtil;
import com.meituan.sqt.utils.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SqtClient {
    private static final Logger log = LoggerFactory.getLogger(SqtClient.class);
    private static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
    private static final String ACCEPT_JSON = "application/json";
    // 测试环境域名
    protected String host = "https://inf-openapi.apigw.test.meituan.com/api/sqt/openapi";
    protected String accessKey;
    protected Long entId;
    protected String secretKey;

    public SqtClient(String accessKey, Long entId, String secretKey) {
        this.accessKey = accessKey;
        this.entId = entId;
        this.secretKey = secretKey;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public String commonPostInvoke(String url, BaseApiRequest request) {
        request.setTs(System.currentTimeMillis()/1000);
        request.setEntId(this.entId);
        try {
            String rawContent = JsonUtil.object2Json(request, false);
            String body = "accessKey=" + getAccessKey() + "&content=" + EncryptUtil.aesEncrypt(rawContent, getSecretKey());
            String responseStr = HttpClientUtil.invokePost(url, body, FORM_URLENCODED, ACCEPT_JSON);
            return responseStr;
        } catch (Exception e) {
            log.error("HTTP调用失败, url:{}", url, e);
        }
        return null;
    }

    public String queryOrderDetail(OrderDetailQueryReq req) {
        return this.commonPostInvoke(this.host + "/queryOrderDetail", req);
    }

    public static void main(String[] args) {
        // 调用
        SqtClient sqtClient = new SqtClient("AB4O3CP2R3ER-TK", 617L, "2Ce0eqKh/ug5+pNnnKAsWA==");

        OrderDetailQueryReq request = new OrderDetailQueryReq();
        request.setSqtBizOrderId(1L);
        String result = sqtClient.queryOrderDetail(request);
    }
}

# SqtClient类依赖的相关类如下:

# HttpClientUtil

// 基于apache httpclient实现
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Properties;

public class HttpClientUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtil.class);

    private static PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
    private static CloseableHttpClient httpClient = null;
    private static CloseableHttpClient httpsClient = null;
    private static HttpClientUtil.HttpConfig httpConfig = null;
    private static SSLConnectionSocketFactory sslsf = null;
    // 连接池最大连接数和每路由最大连接数可通过httpconf.properties进行配置
    private static final String HTTP_CONF_FILE_NAME = "fenxiao-httpconf.properties";
    private static final int MAX_TOTAL_CONNECTION = 800;
    private static final int MAX_PER_ROUTE = 150;
    private static String UTF_8 = "UTF-8";

    interface ContentType {
        String FORM_URLENCODED = "application/x-www-form-urlencoded";
        String JSON = "application/json; charset=utf-8";
    }

    interface AcceptType {
        String ACCEPT_JSON = "application/json";
        String ACCEPT_ANNY = "*/*";
    }

    private HttpClientUtil() {
        // prevent instantialization
    }

    static {
        loadConf();
        httpClientConnectionManager.setMaxTotal(httpConfig.getMaxTotalConnection());  // 连接池最大连接数
        httpClientConnectionManager.setDefaultMaxPerRoute(httpConfig.getMaxPerRoute());  // 每路由最大连接数

        // 通过连接池获取的httpClient
        httpClient = HttpClients.custom().setConnectionManager(httpClientConnectionManager).build();
        // 通过连接池获取的httpsClient, 能支持https
        httpsClient = HttpClients.custom().setSSLSocketFactory(sslsf).setConnectionManager(httpClientConnectionManager).build();
        LOGGER.info("HttpClient initialization");
    }

    /**
     * 如果配置httpconf.properties, 则从该文件中读取http连接池的配置参数, 否则使用默认值
     */
    private static void loadConf() {
        if (httpConfig == null) {
            httpConfig = new HttpConfig();
        }
        Properties properties = new Properties();
        try {
            InputStream inputStream = HttpClientUtil.class.getClassLoader().getResourceAsStream(HTTP_CONF_FILE_NAME);

            if (inputStream == null) {
                LOGGER.warn("httpConfig file={} does not exist", HTTP_CONF_FILE_NAME);
                return;
            }
            properties.load(inputStream);
            int maxTotalConnection = Integer.parseInt(properties.getProperty("max_total_connection"));
            int maxPerRoute = Integer.parseInt(properties.getProperty("max_per_route"));
            LOGGER.info("max_total_connection={}, max_per_route={}", maxTotalConnection, maxPerRoute);
            httpConfig.setMaxTotalConnection(maxTotalConnection);
            httpConfig.setMaxPerRoute(maxPerRoute);
        } catch (IOException e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        } catch (NumberFormatException e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        } catch (Exception e) {
            LOGGER.warn("read httpConfig from file={} failed", HTTP_CONF_FILE_NAME, e);
        }
    }

    private static void initHttps() {
        X509TrustManager trustManager = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            // 初始化SSL上下文
            sslContext.init(null, new TrustManager[]{trustManager}, null);
            // SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        } catch (NoSuchAlgorithmException e) {
            LOGGER.error("初始化https支持失败", e);
        } catch (KeyManagementException e) {
            LOGGER.error("初始化https支持失败", e);
        }
    }

    static class HttpConfig {
        private int maxTotalConnection = MAX_TOTAL_CONNECTION;
        private int maxPerRoute = MAX_PER_ROUTE;

        public int getMaxTotalConnection() {
            return maxTotalConnection;
        }

        public void setMaxTotalConnection(int maxTotalConnection) {
            this.maxTotalConnection = maxTotalConnection;
        }

        public int getMaxPerRoute() {
            return maxPerRoute;
        }

        public void setMaxPerRoute(int maxPerRoute) {
            this.maxPerRoute = maxPerRoute;
        }
    }

    public static String invokePost(String url, String body, String contentType, String accept) throws Exception {
        URIBuilder uriBuilder = new URIBuilder();
        valueForUriBuilder(url, uriBuilder);

        HttpPost httpPost = new HttpPost(uriBuilder.build());

        httpPost.addHeader("Content-type",contentType);
        httpPost.setHeader("Accept", accept);
        httpPost.setEntity(new StringEntity(body, Charset.forName(UTF_8)));

        return sendRequest(url, httpPost);
    }

    private static String sendRequest(String url, HttpRequestBase request) throws Exception {
        CloseableHttpClient client;
        if (url.startsWith("https")) {
            client = httpsClient;
        } else {
            client = httpClient;
        }
        long st = System.currentTimeMillis();
        String responseStr;
        int status = 0;
        try {
            CloseableHttpResponse response = client.execute(request);
            HttpEntity entity = response.getEntity();
            status = response.getStatusLine().getStatusCode();
            if (entity != null) {
                responseStr = EntityUtils.toString(entity, UTF_8);
                response.close();
            } else {
                responseStr = StringUtils.EMPTY;
            }
        } catch (SocketTimeoutException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } catch (ClientProtocolException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } catch (IOException e) {
            LOGGER.error("HttpClient.sendRequest, url:{}, use_time:{} ms",
                    request.getURI(), (System.currentTimeMillis() - st), e);
            throw e;
        } finally {
            request.releaseConnection();
        }
        LOGGER.info("HttpClient Success, status:{}, url:{}, use_time:{} ms",
                status, request.getURI(), (System.currentTimeMillis() - st));
        return responseStr;
    }

    private static void valueForUriBuilder(String url, URIBuilder uriBuilder) {
        Integer apartIndex = url.indexOf("?");
        if (apartIndex == -1) {
            uriBuilder.setPath(url);
        } else {
            uriBuilder.setPath(url.substring(0, apartIndex));
            uriBuilder.setCustomQuery(url.substring(apartIndex + 1, url.length()));
        }
    }
}

# JsonUtil

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class JsonUtil {
    private JsonUtil() {

    }
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
    private static ObjectMapper JSON_TO_OBJECT_MAPPER;
    private static ObjectMapper OBJECT_TO_JSON_MAPPER;  // 对null对象不转换
    private static ObjectMapper OBJECT_TO_JSON_MAPPER_CONTAIN_NULL;  // 包含null对象

    static {
        JSON_TO_OBJECT_MAPPER = new ObjectMapper();
        JSON_TO_OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        OBJECT_TO_JSON_MAPPER_CONTAIN_NULL = new ObjectMapper();
        OBJECT_TO_JSON_MAPPER_CONTAIN_NULL.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

        OBJECT_TO_JSON_MAPPER = new ObjectMapper();
        OBJECT_TO_JSON_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        OBJECT_TO_JSON_MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }
    public static <T> T json2Object(String json, Class<T> clazz) {
        try {
            return json2ObjectThrowException(json, clazz);
        } catch (Exception e) {
            LOGGER.error("json error", e);
            return null;
        }
    }

    public static <T> T json2Object(String json, TypeReference typeReference) {
        try {
            return json2ObjectThrowException(json, typeReference);
        } catch (Exception e) {
            LOGGER.error("json error", e);
            return null;
        }
    }

    public static <T> List<T> json2ObjectList(String json, Class<T> clazz) {
        try {
            return json2ObjectListThrowException(json, clazz);
        } catch (Exception e) {
            LOGGER.error("json error", e);
            return null;
        }
    }

    public static String object2Json(Object object) {
        return object2Json(object, false);
    }

    public static String object2Json(Object object, boolean containNull) {
        try {
            return object2JsonThrowException(object, containNull);
        } catch (Exception e) {
            LOGGER.error("json error", e);
            return null;
        }
    }

    public static Map json2Map(String json) {
        try {
            return json2MapThrowException(json);
        } catch (Exception e) {
            LOGGER.error("json error", e);
            return null;
        }
    }

    public static <T> T json2ObjectThrowException(String json, Class<T> clazz) throws Exception{
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            return JSON_TO_OBJECT_MAPPER.readValue(json, clazz);
        } catch (Exception e) {
            LOGGER.error("json2Object error, json: {}", json, e);
            throw e;
        }

    }

    public static <T> T json2ObjectThrowException(String json, TypeReference typeReference) throws Exception{
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            return JSON_TO_OBJECT_MAPPER.readValue(json, typeReference);
        } catch (Exception e) {
            LOGGER.error("json2Object error, json: {}", json, e);
            throw e;
        }

    }

    public static <T> List<T> json2ObjectListThrowException(String json, Class<T> clazz) throws IOException {
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            List<T> objects = JSON_TO_OBJECT_MAPPER.readValue(json, TypeFactory.defaultInstance().constructCollectionType(List.class, clazz));
            return objects;
        } catch (Exception e) {
            LOGGER.error("json2ObjectList error, json: {}", json, e);
            throw e;
        }
    }

    public static String object2JsonThrowException(Object object, boolean containNull) throws Exception{
        if (object == null) {
            return "";
        }
        try {
            if (!containNull) {
                return OBJECT_TO_JSON_MAPPER.writeValueAsString(object);
            } else {
                return OBJECT_TO_JSON_MAPPER_CONTAIN_NULL.writeValueAsString(object);
            }
        } catch (Exception e) {
            LOGGER.error("object2Json error", e);
            throw e;
        }
    }

    public static Map json2MapThrowException(String json) throws Exception{
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            return JSON_TO_OBJECT_MAPPER.readValue(json, Map.class);
        } catch (Exception e) {
            LOGGER.error("json2Map error, json: {}", json, e);
            throw e;
        }
    }
}

# OrderDetailQueryReq

public class OrderDetailQueryReq extends BaseApiRequest {
    private Long sqtBizOrderId;

    public Long getSqtBizOrderId() {
        return sqtBizOrderId;
    }

    public void setSqtBizOrderId(Long sqtBizOrderId) {
        this.sqtBizOrderId = sqtBizOrderId;
    }
}

# BaseApiRequest

import javax.validation.constraints.NotNull;

public class BaseApiRequest {
    @NotNull(message = "不允许为空")
    private Long ts;
    @NotNull(message = "不允许为空")
    private Long entId;

    public Long getTs() {
        return ts;
    }

    public void setTs(Long ts) {
        this.ts = ts;
    }

    public Long getEntId() {
        return entId;
    }

    public void setEntId(Long entId) {
        this.entId = entId;
    }
}
上次更新: 6/29/2026, 7:56:38 PM