本节分享给大家通过调用腾讯云API实现语音合成技术

 

package com.example.combat.controller;

import com.example.combat.service.TTSService;
import com.example.combat.ttsutis.param.TextToVoice;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

/**
 * @description: 语音合成
 * @author:zhucj
 * @date: 2019-11-27 16:37
 */
@RestController
@RequestMapping("/tts")
public class TTSControllerl {

    @Autowired
    private TTSService ttsService;

    @ApiOperation(value = "语音合成")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "text",value = "合成语音的源文本",required = true,dataType = "String"),
            @ApiImplicitParam(name = "volume",value = "音量大小,范围:[0,10]",required = true,dataType = "String"),
            @ApiImplicitParam(name = "speed",value = "语速,范围:[-2,2]",required = true,dataType = "String"),
            @ApiImplicitParam(name = "voiceType",value = "音色",required = true,dataType = "Integer")
    })
    @PostMapping(value = "textToVoice")
    public void textToVoice(@Valid @RequestBody TextToVoice textToVoice, HttpServletResponse response){
        ttsService.voiceSynthesis(textToVoice,response);
    }
}
package com.example.combat.service;

import com.example.combat.ttsutis.R;
import com.example.combat.ttsutis.param.TextToVoice;

import javax.servlet.http.HttpServletResponse;

/**
 * @description: 语音合成实现类
 * @author: zhucj
 * @date: 2019-11-27 16:12
 */
public interface TTSService {

    /**
     * 语音合成
     * @param textToVoice
     */
    void voiceSynthesis(TextToVoice textToVoice, HttpServletResponse response);
}
package com.example.combat.service.Impl;

import cn.hutool.core.io.FileUtil;
import com.example.combat.afsutils.Base64ConvertUtils;
import com.example.combat.gaodemapUtils.SystemConstant;
import com.example.combat.service.TTSService;
import com.example.combat.ttsutis.R;
import com.example.combat.ttsutis.TTSUtil;
import com.example.combat.ttsutis.param.TextToVoice;
import com.example.combat.ttsutis.param.TextToVoiceResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Decoder;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Objects;

/**
 * @description:
 * @author: zhucj
 * @date: 2019-11-27 16:15
 */
@Service
@Slf4j
public class TTSServiceImpl implements TTSService {

    @Autowired
    private TTSUtil ttsUtil;

    @Value("${tencent.pathImg}")
    private String pathImg;

    @Override
    public void voiceSynthesis(TextToVoice textToVoice, HttpServletResponse response) {
        R r = ttsUtil.voiceSynthesis(textToVoice);
        if (r.getSuccess() && Objects.equals(r.getCode(), SystemConstant.SUCCESS_CODE)){
            TextToVoiceResponse data =(TextToVoiceResponse) r.getData();
            String audio = data.getAudio();
            //将base64编码的wav/mp3音频数据 下载给前端
            exportImg(response,pathImg,audio);
        }else {
            throw new RuntimeException("语音合成失败");
        }
    }

    public static void exportImg(HttpServletResponse response, String filePath,String imgStr) {

        //对字节数组字符串进行Base64解码并生成图片
        String imgFilePath = null;
        //图像数据为空
        if (imgStr == null){
            return ;
        }
        BASE64Decoder decoder = new BASE64Decoder();
        BufferedOutputStream buff = null;
        ServletOutputStream outStr = null;
        try {
            //Base64解码
            byte[] b = decoder.decodeBuffer(imgStr);
            for (int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    //调整异常数据
                    b[i] += 256;
                }
            }
            response.setCharacterEncoding("utf-8");
            //设置响应的内容类型
            response.setContentType("text/plain");
            //设置文件的名称和格式
            response.setHeader("content-type", "application/octet-stream");
            response.setContentType("application/octet-stream");
            response.addHeader("Content-Disposition", "attachment;filename=" + "合成语音.wav");
            outStr = response.getOutputStream();
            buff = new BufferedOutputStream(outStr);
            buff.write(b);
            buff.flush();
            buff.close();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("文件导出异常:", e);
        } finally {
            try {
                buff.close();
            } catch (Exception e) {
                log.error("流关闭异常:", e);
            }
            try {
                outStr.close();
            } catch (Exception e) {
                log.error("流关闭异常:", e);
            }
        }
    }
}
package com.example.combat.ttsutis;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.combat.afsutils.HttpUtil;
import com.example.combat.afsutils.SignUtils;
import com.example.combat.asrutils.param.SentenceRecognitionApi;
import com.example.combat.asrutils.param.SentenceResponse;
import com.example.combat.asrutils.param.SystemConstants;
import com.example.combat.config.constant.ContentTypeEnum;
import com.example.combat.config.constant.HttpMethodEnum;
import com.example.combat.config.constant.SignMenodEnum;
import com.example.combat.ttsutis.param.TextToVoice;
import com.example.combat.ttsutis.param.TextToVoiceResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;

/**
 * @description: 语音合成
 * @author: zhucj
 * @date: 2019-11-27 15:35
 */
@Component
@Slf4j
public class TTSUtil {

    @Value("${tencent.secretId}")
    private String sercretId;

    @Value("${tencent.secretKey}")
    private String sercretKey;

    /**
     * 语音合成
     * @param textToVoice
     * @return
     */
   public R voiceSynthesis(TextToVoice textToVoice){

       TreeMap treeMap = createPublicMap("TextToVoice", "2019-08-23", "ap-shenzhen-fsi");
       HashMap<String,Object> hashMap = new HashMap<>();
       try {
           hashMap.put("Text", URLEncoder.encode(textToVoice.getText(),"UTF-8"));
       } catch (UnsupportedEncodingException e) {
          log.error("URL Encoder异常:{}",e.getMessage());
          return R.error("语音合成失败").setCode(SystemConstants.SERVER_ERROR_CODE);
       }
       hashMap.put("SessionId", UUID.randomUUID().toString());
       hashMap.put("ModelType",1);
       hashMap.put("Volume",Float.valueOf(textToVoice.getVolume()));
       hashMap.put("Speed",Float.valueOf(textToVoice.getSpeed()));
       hashMap.put("VoiceType",textToVoice.getVoiceType());

       //签名,公共参数不需要放到body中
       String sign = null;
       try {
           sign = SignUtils.sign(treeMap, HttpMethodEnum.POST, SignMenodEnum.TC3_HMAC_SHA256, JSON.toJSONString(hashMap)
                   , TextToVoiceConstant.TEXT_TO_VOICE, sercretKey, ContentTypeEnum.JSON);
       } catch (Exception e) {
           log.error("签名异常:{}",e.getMessage());
           return R.error("签名异常").setCode(SystemConstants.SERVER_ERROR_CODE);
       }
       try {
           String respJson = HttpUtil.httpPost(TextToVoiceConstant.TEXT_TO_VOICE, JSON.parseObject(sign, Map.class),hashMap);
           JSONObject jsonObject = JSON.parseObject(respJson);
           String response = jsonObject.getString("Response");
           JSONObject error =(JSONObject) JSON.parseObject(response).get("Error");
           if (Objects.nonNull(error)){
               return R.error(String.valueOf(error.get("Message"))).setCode(SystemConstants.SERVER_ERROR_CODE);
           }else {
               TextToVoiceResponse textToVoiceResponse = JSON.parseObject(response, TextToVoiceResponse.class);
               return R.ok(textToVoiceResponse.getAudio()).setCode(SystemConstants.SUCCESS_CODE);
           }

       } catch (Exception e) {
           log.error("语音合成失败:{}",e.getMessage());
           return R.error("语音合成失败").setCode(SystemConstants.SERVER_ERROR_CODE);
       }
   }
    /**
     * 封装请求公共参数
     * @param action
     * @param version
     * @return
     */
    public TreeMap createPublicMap(String action, String version,String region ){

        TreeMap<String,Object> treeMap = new TreeMap<>();
        treeMap.put("Action",action);
        treeMap.put("Version",version);
        treeMap.put("Region",region);
        treeMap.put("Timestamp",getCurrentTimestamp());
        treeMap.put("Nonce",new Random().nextInt(Integer.MAX_VALUE));
        treeMap.put("SecretId",sercretId);
        return treeMap;
    }

    /**
     * 获取当前时间戳,单位秒
     * @return
     */
    public static long getCurrentTimestamp() {
        return System.currentTimeMillis()/1000;
    }

}
package com.example.combat.ttsutis;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.ToString;

import java.io.Serializable;

/**
 * 返回类型
 * @author choleece
 * @date 2018/9/27
 */
@ApiModel
@ToString
public class R<T> implements Serializable {

    private static final long serialVersionUID = -6287952131441663819L;

    /**
     * 编码
     */
    @ApiModelProperty(value = "响应码", example = "200")
    private int code = 200;

    /**
     * 成功标志
     */
    @ApiModelProperty(value = "成功标志", example = "true")
    private Boolean success;

    /**
     * 返回消息
     */
    @ApiModelProperty(value = "返回消息说明", example = "操作成功")
    private String msg="操作成功";

    /**
     * 返回数据
     */
    @ApiModelProperty(value = "返回数据")
    private T data;

    /**
     * 创建实例
     * @return
     */
    public static R instance() {
        return new R();
    }

    public int getCode() {
        return code;
    }

    public R setCode(int code) {
        this.code = code;
        return this;
    }

    public Boolean getSuccess() {
        return success;
    }

    public R setSuccess(Boolean success) {
        this.success = success;
        return this;
    }

    public String getMsg() {
        return msg;
    }

    public R setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public T getData() {
        return data;
    }
    public R setData(T data) {
        this.data = data;
        return this;
    }

    public static R ok() {
        return R.instance().setSuccess(true);
    }

    public static R ok(Object data) {
        return ok().setData(data);
    }

    public static R ok(Object data, String msg) {
        return ok(data).setMsg(msg);
    }

    public static R error() {
        return R.instance().setSuccess(false);
    }

    public static R error(String msg) {
        return error().setMsg(msg);
    }

    /**
     * 无参
     */
    public R() {
    }

    public R(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public R(int code, T data){
        this.code = code;
        this.data = data;
    }

    /**
     * 有全参
     * @param code
     * @param msg
     * @param data
     * @param success
     */
    public R(int code, String msg, T data, Boolean success) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.success = success;
    }

    /**
     * 有参
     * @param code
     * @param msg
     * @param data
     */
    public R(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

  
}
package com.example.combat.ttsutis;

/**
 * @description: 语音合成常量
 * @author: zhucj
 * @date: 2019-11-27 15:51
 */
public class TextToVoiceConstant {

    /**
     * 语音合成Api
     */
    public static final String TEXT_TO_VOICE = "https://tts.ap-shenzhen-fsi.tencentcloudapi.com";
}
package com.example.combat.ttsutis.param;

import io.swagger.annotations.ApiModel;
import lombok.*;

import javax.validation.constraints.NotNull;

/**
 * @description:
 * @author: zhucj
 * @date: 2019-11-27 15:39
 */
@Data
@ApiModel(description = "语音合成请求实体")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class TextToVoice {


    /**
     * 合成语音的源文本,按UTF-8编码统一计算。
     * 中文最大支持100个汉字(全角标点符号算一个汉字);
     * 英文最大支持400个字母(半角标点符号算一个字母)。包含空格等字符时需要url encode再传输。
     */
    @NotNull(message = "合成语音的源文本不为空")
    private String text;

    /**
     * 音量大小,范围:[0,10],分别对应11个等级的音量,默认为0
     */
    @NotNull(message = "音量大小不为空")
    private String volume;

    /**
     *语速,范围:[-2,2],分别对应不同语速:
     * -2代表0.6倍
     * -1代表0.8倍
     * 0代表1.0倍(默认)
     * 1代表1.2倍
     * 2代表1.5倍
     * 输入除以上整数之外的其他参数不生效,按默认值处理。
     */
    @NotNull(message = "语速大小不为空")
    private String speed;

    /**
     * 音色
     * 0-亲和女声(默认)
     * 1-亲和男声
     * 2-成熟男声
     * 4-温暖女声
     * 5-情感女声
     * 6-情感男声
     */
    @NotNull(message = "音色选择不为空")
    private Integer voiceType;

}
package com.example.combat.ttsutis.param;

import com.example.combat.afsutils.param.resp.Response;
import io.swagger.annotations.ApiModel;
import lombok.*;

/**
 * @description:
 * @author: zhucj
 * @date: 2019-11-27 16:09
 */
@Data
@ApiModel(description = "语音合成响应实体")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class TextToVoiceResponse extends Response {

    private String Audio;

    private String SessionId;
}
package com.example.combat.afsutils;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
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.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
public class HttpUtil {

    public static final ContentType TEXT_PLAIN = ContentType.create("text/plain", StandardCharsets.UTF_8);

    /**
     * HttpClient 连接池
     */
    private static PoolingHttpClientConnectionManager cm = null;

    static {
        // 初始化连接池,可用于请求HTTP/HTTPS(信任所有证书)
        cm = new PoolingHttpClientConnectionManager(getRegistry());
        // 整个连接池最大连接数
        cm.setMaxTotal(200);
        // 每路由最大连接数,默认值是2
        cm.setDefaultMaxPerRoute(5);
    }

    /**
     * 发送 HTTP GET请求
     * <p>不带请求参数和请求头</p>
     * @param url 地址
     * @return
     * @throws Exception
     */
    public static String httpGet(String url) throws Exception {
        log.info("请求参数:{}",url);
        HttpGet httpGet = new HttpGet(url);

        return doHttp(httpGet);
    }

    /**
     * 发送 HTTP GET请求
     * <p>带请求参数,不带请求头</p>
     * @param url    地址
     * @param params 参数
     * @return
     * @throws Exception
     * @throws Exception
     */
    public static String httpGet(String url, Map<String, Object> params) throws Exception {
        // 转换请求参数
        List<NameValuePair> pairs = covertParams2NVPS(params);

        // 装载请求地址和参数
        URIBuilder ub = new URIBuilder();
        ub.setPath(url);
        ub.setParameters(pairs);

        HttpGet httpGet = new HttpGet(ub.build());

        return doHttp(httpGet);
    }

    /**
     * 发送 HTTP GET请求
     * <p>带请求参数和请求头</p>
     * @param url     地址
     * @param headers 请求头
     * @param params  参数
     * @return
     * @throws Exception
     * @throws Exception
     */
    public static String httpGet(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception {
        // 转换请求参数
        List<NameValuePair> pairs = covertParams2NVPS(params);

        // 装载请求地址和参数
        URIBuilder ub = new URIBuilder();
        ub.setPath(url);
        ub.setParameters(pairs);

        HttpGet httpGet = new HttpGet(ub.build());
        // 设置请求头
        for (Map.Entry<String, Object> param : headers.entrySet()){
            httpGet.addHeader(param.getKey(), String.valueOf(param.getValue()));}

        return doHttp(httpGet);
    }

    /**
     * 发送 HTTP POST请求
     * <p>不带请求参数和请求头</p>
     *
     * @param url 地址
     * @return
     * @throws Exception
     */
    public static String httpPost(String url) throws Exception {
        HttpPost httpPost = new HttpPost(url);

        return doHttp(httpPost);
    }

    /**
     * 发送 HTTP POST请求
     * <p>带请求参数,不带请求头</p>
     *
     * @param url    地址
     * @param params 参数
     * @return
     * @throws Exception
     */
    public static String httpPost(String url, Map<String, Object> params) throws Exception {
        // 转换请求参数
        List<NameValuePair> pairs = covertParams2NVPS(params);

        HttpPost httpPost = new HttpPost(url);
        // 设置请求参数
        httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8.name()));

        return doHttp(httpPost);
    }

    /**
     * 发送 HTTP POST请求
     * <p>带请求参数和请求头</p>
     *
     * @param url     地址
     * @param headers 请求头
     * @param params  参数
     * @return
     * @throws Exception
     */
    public static String httpPost(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception {
        log.info("POST请求参数:{}",params);
        HttpPost httpPost = new HttpPost(url);
        // 设置请求参数
        StringEntity entity = new StringEntity(JSON.toJSONString(params),ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);
        // 设置请求头
        for (Map.Entry<String, Object> param : headers.entrySet()){
            httpPost.addHeader(param.getKey(), String.valueOf(param.getValue()));}

        return doHttp(httpPost);
    }




    /**
     * 转换请求参数
     *
     * @param params
     * @return
     */
    public static List<NameValuePair> covertParams2NVPS(Map<String, Object> params) {
        List<NameValuePair> pairs = new ArrayList<NameValuePair>();

        for (Map.Entry<String, Object> param : params.entrySet()){
            pairs.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));}

        return pairs;
    }

    /**
     * 发送 HTTP 请求
     *
     * @param request
     * @return
     * @throws Exception
     */
    private static String doHttp(HttpRequestBase request) throws Exception {
        // 通过连接池获取连接对象
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();

        return doRequest(httpClient, request);
    }

    /**
     * 发送 HTTPS 请求
     * <p>使用指定的证书文件及密码</p>
     *
     * @param request
     * @param path
     * @param password
     * @return
     * @throws Exception
     * @throws Exception
     */
    private static String doHttps(HttpRequestBase request, String path, String password) throws Exception {
        // 获取HTTPS SSL证书
        SSLConnectionSocketFactory csf = getSSLFactory(path, password);
        // 通过连接池获取连接对象
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();

        return doRequest(httpClient, request);
    }

    /**
     * 获取HTTPS SSL连接工厂
     * <p>使用指定的证书文件及密码</p>
     *
     * @param path     证书全路径
     * @param password 证书密码
     * @return
     * @throws Exception
     * @throws Exception
     */
    private static SSLConnectionSocketFactory getSSLFactory(String path, String password) throws Exception {

        // 初始化证书,指定证书类型为“PKCS12”
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // 读取指定路径的证书
        FileInputStream input = new FileInputStream(new File(path));

        try {
            // 装载读取到的证书,并指定证书密码
            keyStore.load(input, password.toCharArray());
        } finally {
            input.close();
        }

        // 获取HTTPS SSL证书连接上下文
        SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, password.toCharArray()).build();

        // 获取HTTPS连接工厂,指定TSL版本
        SSLConnectionSocketFactory sslCsf = new SSLConnectionSocketFactory(sslContext, new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.2"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());

        return sslCsf;
    }

    /**
     * 获取HTTPS SSL连接工厂
     * <p>跳过证书校验,即信任所有证书</p>
     *
     * @return
     * @throws Exception
     */
    private static SSLConnectionSocketFactory getSSLFactory() throws Exception {
        // 设置HTTPS SSL证书信息,跳过证书校验,即信任所有证书请求HTTPS
        SSLContextBuilder sslBuilder = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
            public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                return true;
            }
        });

        // 获取HTTPS SSL证书连接上下文
        SSLContext sslContext = sslBuilder.build();

        // 获取HTTPS连接工厂
        SSLConnectionSocketFactory sslCsf = new SSLConnectionSocketFactory(sslContext, new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.2"}, null, NoopHostnameVerifier.INSTANCE);

        return sslCsf;
    }

    /**
     * 获取 HTTPClient注册器
     *
     * @return
     * @throws Exception
     */
    private static Registry<ConnectionSocketFactory> getRegistry() {
        Registry<ConnectionSocketFactory> registry = null;

        try {
            registry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", new PlainConnectionSocketFactory()).register("https", getSSLFactory()).build();
        } catch (Exception e) {
            log.error("获取 HTTPClient注册器失败", e);
        }

        return registry;
    }


    /**
     * 处理Http/Https请求,并返回请求结果
     * <p>注:默认请求编码方式 UTF-8</p>
     *
     * @param httpClient
     * @param request
     * @return
     * @throws Exception
     */
    private static String doRequest(CloseableHttpClient httpClient, HttpRequestBase request) throws Exception {
        String result = null;
        CloseableHttpResponse response = null;

        try {
            // 获取请求结果
            response = httpClient.execute(request);
            // 解析请求结果
            HttpEntity entity = response.getEntity();
            // 转换结果
            result = EntityUtils.toString(entity, StandardCharsets.UTF_8.name());
            // 关闭IO流
            EntityUtils.consume(entity);
        } finally {
            if (null != response){
                response.close();}
        }

        return result;
    }
}
package com.example.combat.afsutils;

import com.alibaba.fastjson.JSON;
import com.example.combat.config.constant.ContentTypeEnum;
import com.example.combat.config.constant.HttpMethodEnum;
import com.example.combat.config.constant.SignMenodEnum;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
import java.util.TreeMap;

/**
 * @description: 腾讯云 签名方法
 * @author: zhucj
 * @date: 2019-10-18 14:14
 */
@Slf4j
public class SignUtils {


    private final static String CHARSET = "UTF-8";

    private final static Charset UTF8 = StandardCharsets.UTF_8;




    public static String sign(TreeMap<String, Object> params, HttpMethodEnum menth, SignMenodEnum signMenodEnum,
                              String jsonString, String reqUrl, String sercretKey, ContentTypeEnum typeEnum) throws Exception {
        String signString = null;
        String  sercretId  = String.valueOf(params.get("SecretId"));

        switch (signMenodEnum){
            case TC3_HMAC_SHA256:
                String replace = reqUrl.replace("https://", "");
                String service = replace.substring(0,3);
                String host = replace;
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                // 注意时区,否则容易出错
                sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
                String date = sdf.format(new Date(Long.valueOf(params.get("Timestamp") + "000")));
                // ************* 步骤 1:拼接规范请求串 *************
                String canonicalUri = "/";
                String canonicalQueryString = "";
                String canonicalHeaders = "content-type:"+typeEnum.getName() +"\n" + "host:" + host + "\n";
                String signedHeaders = "content-type;host";

                String hashedRequestPayload = sha256Hex(jsonString);
                String canonicalRequest = menth.getName() + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
                        + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;

                // ************* 步骤 2:拼接待签名字符串 *************
                String credentialScope = date + "/" + service + "/" + "tc3_request";
                String hashedCanonicalRequest = sha256Hex(canonicalRequest);
                String stringToSign = signMenodEnum.getMendoName() + "\n" + params.get("Timestamp") + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
                log.info("待签名参数:{}",stringToSign);

                // ************* 步骤 3:计算签名 *************
                byte[] secretDate = hmac256(("TC3" +sercretKey).getBytes(UTF8), date);
                byte[] secretService = hmac256(secretDate, service);
                byte[] secretSigning = hmac256(secretService, "tc3_request");
                String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
                log.info("生成签名参数:{}",signature);

                // ************* 步骤 4:拼接 Authorization *************
                String authorization = signMenodEnum.getMendoName() + " " + "Credential=" + sercretId + "/" + credentialScope + ", "
                        + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
                log.info("生成authorization参数:{}",authorization);

                TreeMap<String, String> headers = new TreeMap<String, String>();
                headers.put("Authorization", authorization);
                headers.put("Content-Type",typeEnum.getName());
                headers.put("Host", host);
                headers.put("X-TC-Action",String.valueOf(params.get("Action")) );
                headers.put("X-TC-Timestamp",String.valueOf(params.get("Timestamp")));
                headers.put("X-TC-Version",String.valueOf(params.get("Version")));
                if (Objects.nonNull(params.get("Region"))){
                    headers.put("X-TC-Region",String.valueOf(params.get("Region")));
                }
                signString = JSON.toJSONString(headers);

                break;
                default:
                    StringBuilder s2s = new StringBuilder(reqUrl.replace("https://",menth.getName())+"/?");
                    // 签名时要求对参数进行字典排序,此处用TreeMap保证顺序
                    for (String k : params.keySet()) {
                        s2s.append(k).append("=").append(params.get(k).toString()).append("&");
                    }
                    String s = s2s.toString().substring(0, s2s.length() - 1);
                    Mac mac = Mac.getInstance(signMenodEnum.getMendoName());
                    SecretKeySpec secretKeySpec = new SecretKeySpec(sercretKey.getBytes(CHARSET), mac.getAlgorithm());
                    mac.init(secretKeySpec);
                    byte[] hash = mac.doFinal(s.getBytes(CHARSET));
                    signString = DatatypeConverter.printBase64Binary(hash);
                    break;
        }
        return signString ;


    }

    /**
     * 获取签名之后的请求Url
     * @param params
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getUrl(TreeMap<String, Object> params,String reqUrl) throws UnsupportedEncodingException {
        StringBuilder url = new StringBuilder(reqUrl+"/?");
        // 实际请求的url中对参数顺序没有要求
        for (String k : params.keySet()) {
            // 需要对请求串进行urlencode,由于key都是英文字母,故此处仅对其value进行urlencode
            url.append(k).append("=").append(URLEncoder.encode(params.get(k).toString(), CHARSET)).append("&");
        }
        return url.toString().substring(0, url.length() - 1);
    }

    public static String getUrl(TreeMap<String, Object> params,String reqUrl,String jsonString,ContentTypeEnum typeEnum){
        String replace = reqUrl.replace("https://", "");
        StringBuilder sb = new StringBuilder();
        sb.append("curl -X POST https://").append(replace)
                .append(" -H \"Authorization: ").append(params.get("Authorization")).append("\"")
                .append(" -H \"Content-Type:").append(typeEnum.getName())
                .append(" -H \"Host: ").append(replace).append("\"")
                .append(" -H \"X-TC-Action: ").append(params.get("Action")).append("\"")
                .append(" -H \"X-TC-Timestamp: ").append(params.get("Timestamp")).append("\"")
                .append(" -H \"X-TC-Version: ").append(params.get("Version")).append("\"");
        if (Objects.nonNull(params.get("Region"))){
            sb.append(" -H \"X-TC-Region: ").append(params.get("Region")).append("\"");
        }
        sb.append(" -d '").append(jsonString).append("'");
        return sb.toString();
    }


    public static byte[] hmac256(byte[] key, String msg) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
        mac.init(secretKeySpec);
        return mac.doFinal(msg.getBytes(UTF8));
    }


    public static String sha256Hex(String s) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] d = md.digest(s.getBytes(UTF8));
        return DatatypeConverter.printHexBinary(d).toLowerCase();
    }


}
#腾讯云服务配置
tencent:
  secretId: *******
  secretKey: *******
  pathImg: E:/upload/