微信支付API V3(.Net Core)

    public class WXPayService : IPayService
    {
        public static readonly ILogger _logger = LogManager.GetCurrentClassLogger();

        private static char[] constant =
                          {
                                '0','1','2','3','4','5','6','7','8','9',
                                'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
                                'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
                              };

        /// <summary>
        /// 
        /// </summary>
        protected ReceivablePayConfig RecePayCfg { get; set; }

        public WXPayService(IOptions<ReceivablePayConfig> recePayCfgOpt)
        {
            this.RecePayCfg = recePayCfgOpt.Value;
        }

        //public async Task<PayCallBackOutput> 




        public string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
        {
            GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
            AeadParameters aeadParameters = new AeadParameters(
                new KeyParameter(Encoding.UTF8.GetBytes(this.RecePayCfg.AES_KEY)),
                128,
                Encoding.UTF8.GetBytes(nonce),
                Encoding.UTF8.GetBytes(associatedData));
            gcmBlockCipher.Init(false, aeadParameters);

            byte[] data = Convert.FromBase64String(ciphertext);
            byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
            int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
            gcmBlockCipher.DoFinal(plaintext, length);
            return Encoding.UTF8.GetString(plaintext);
        }

        private string ToUrl(IEnumerable<KeyValuePair<string, string>> parameters)
        {
            string buff = "";
            foreach (KeyValuePair<string, string> pair in parameters)
            {
                if (string.IsNullOrEmpty(pair.Value))
                {
                    continue;
                }

                if (pair.Key != "sign" && pair.Value.ToString() != "")
                {
                    buff += pair.Key + "=" + pair.Value + "&";
                }
            }
            buff = buff.Trim('&');
            return buff;
        }

        private string GenerateRandomNumber(int length)
        {
            StringBuilder newRandom = new StringBuilder(62);
            Random rd = new Random();
            for (int i = 0; i < length; i++)
            {
                newRandom.Append(constant[rd.Next(62)]);
            }
            return newRandom.ToString();
        }

        private static long ToUnixEpochDate(DateTime date)
    => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

        /// <summary>
        /// 统一下单v3(https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml)
        /// </summary>
        /// <typeparam name="I"></typeparam>
        /// <param name="outData"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> UnifiedOrder<I>(I outData, params KeyValuePair<string, string>[] parameters) where I : new()
        {
            _logger.Info(new LogInfo() { Method = "UnifiedOrder", Argument = parameters, Description = "统一下单3" });
            //判断是否存在参数
            if (parameters?.Length <= 0)
            {
                throw new Exception("未提供参数");
            }

            string openid = string.Empty;
            if (parameters.Count(t => string.Compare(t.Key, "openid", true) == 0 && !string.IsNullOrWhiteSpace(t.Value)) <= 0)
            {
                if (parameters.Count(t => string.Compare(t.Key, "code", true) == 0 && !string.IsNullOrWhiteSpace(t.Value)) <= 0)
                {
                    throw new Exception("未提供code参数");
                }
            }
            else
            {
                openid = parameters.FirstOrDefault(t => string.Compare(t.Key, "openid", true) == 0).Value;
            }
            var newparams = parameters.ToList();
            newparams.Add(new KeyValuePair<string, string>("appid", RecePayCfg.appid));
            newparams.Add(new KeyValuePair<string, string>("secret", RecePayCfg.secret));
            newparams.Add(new KeyValuePair<string, string>("grant_type", RecePayCfg.grant_type));
            WXUnifiedOrderReq req = new WXUnifiedOrderReq();
            Dictionary<string, string> sParams2 = new Dictionary<string, string>();
            //判断是否存在openid
            if (!string.IsNullOrWhiteSpace(openid))
            {
                req.payer = new Payer() { openid = openid };
                sParams2.Add("openid", openid);
            }
            else
            {
                string url = this.RecePayCfg.url + this.ToUrl(newparams.OrderBy(t => t.Key));
                using (HttpClient client = new HttpClient())
                {
                    var response = await client.GetAsync(url);
                    string contentstr = await response.Content.ReadAsStringAsync();
                    if (response.IsSuccessStatusCode)
                    {
                        WXCodeResult codeResult = null;
                        try
                        {
                            codeResult = JsonConvert.DeserializeObject<WXCodeResult>(contentstr);
                        }
                        catch (Exception ex)
                        {
                            _logger.Error(ex, "微信支付获取openid失败");
                            throw new SinoException("获取OpenId失败");
                        }

                        if (codeResult == null || string.IsNullOrEmpty(codeResult.openid))
                        {
                            _logger.Error(new SinoException(contentstr), "微信支付获取openid失败");
                            throw new SinoException("获取OpenId失败");
                        }
                        req.payer = new Payer() { openid = codeResult.openid };
                        sParams2.Add("openid", codeResult.openid);
                    }
                    else
                    {
                        _logger.Error(new SinoException(contentstr), "微信支付获取openid失败");
                        throw new SinoException("获取OpenId失败");
                    }
                }
            }
            req.appid = RecePayCfg.appid;
            req.mchid = RecePayCfg.mchid;
            req.notify_url = RecePayCfg.notify_url;
            if (outData is DependOutDataWXPay)
            {
                var tempData = outData as DependOutDataWXPay;
                req.amount = new Amount() { total = tempData.total };
                req.description = tempData.description;
                req.out_trade_no = tempData.out_trade_no;
            }
            else
            {
                throw new Exception("入参类型不正确");
            }
            var nonceStr = GenerateRandomNumber(32);
            using (var httpClient = new HttpClient(new WXPayHttpHandler(RecePayCfg.mchid, RecePayCfg.serial_no, "apiclient_cert.p12", RecePayCfg.mchid, nonceStr)))
            {
                string postUrl = RecePayCfg.UnifiedOrderUrl;
                // POST 方式
                //一定要这样传递参数,不然在加密签名的时候获取到的参数就是\u0这种形式的数据了,不是传递的这样的数据了,导致加密的结果不正确

                string jsonData = JsonConvert.SerializeObject(req);
                _logger.Info($"统一下单参数:{jsonData}");
                var bodyJson = new StringContent(jsonData, Encoding.UTF8, "application/json");
                httpClient.Timeout = TimeSpan.FromSeconds(10);
                HttpResponseMessage unifiedOrderRes = null;
                try
                {
                    unifiedOrderRes = await httpClient.PostAsync(postUrl, bodyJson);
                }
                catch (Exception ex)
                {
                    throw new SinoException("微信统一下单失败", ex);
                }
                _logger.Info("httpclient请求结束");
                // prepay_id。
                var postResult = await unifiedOrderRes.Content.ReadAsStringAsync();
                _logger.Info($"httpclient获取结果{postResult}");
                if (!unifiedOrderRes.IsSuccessStatusCode)
                {
                    _logger.Error(new Exception(postResult), "微信统一下单失败");
                    throw new SinoException("微信统一下单失败");
                }
                UnifiedOrderRes unifiedOrder = JsonConvert.DeserializeObject<UnifiedOrderRes>(postResult);

                //二次签名
                string timestamp = ToUnixEpochDate(DateTime.Now).ToString();

                string package = $"prepay_id={unifiedOrder.prepay_id}";
                sParams2.Add(nameof(RecePayCfg.appid), RecePayCfg.appid);
                sParams2.Add("nonceStr", nonceStr);
                sParams2.Add("timeStamp", timestamp);
                sParams2.Add("package", package);
                sParams2.Add("signType", "RSA");
                //需要签名的字符串
                string needSignStr = $"{RecePayCfg.appid}
{timestamp}
{nonceStr}
{package}
";
                _logger.Info($"paySign签名前:{needSignStr}");
                string paySign = needSignStr.WXRSAWithSHA256Sign("apiclient_cert.p12", RecePayCfg.mchid);
                _logger.Info($"paySign签名结果:{paySign}");
                sParams2.Add("paySign", paySign);
                return sParams2;
            }
        }

        /// <summary>
        /// 商户订单查询
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<DecryptionCallBack> PayOrderSearch<T>(T input) where T : new()
        {
            DecryptionCallBack retVal = null;
            PayOrderSearch result = null;
            if (input is PayOrderSearch)
            {
                result = input as PayOrderSearch;
            }
            else
            {
                throw new Exception("入参类型不正确");
            }
            var nonceStr = GenerateRandomNumber(32);
            using (var httpClient = new HttpClient(new WXPayHttpHandler(RecePayCfg.mchid, RecePayCfg.serial_no, "apiclient_cert.p12", RecePayCfg.mchid, nonceStr)))
            {
                string getUrl = $"{RecePayCfg.PayResultSearchUrl}{result.out_trade_no}?mchid={RecePayCfg.mchid}";
                var httpResponse = await httpClient.GetAsync(getUrl);
                string contentstr = await httpResponse.Content.ReadAsStringAsync();
                if (httpResponse.IsSuccessStatusCode)
                {
                    retVal = JsonConvert.DeserializeObject<DecryptionCallBack>(contentstr);
                }
                else
                {
                    _logger.Error(new SinoException(contentstr), "查询微信订单失败");
                    throw new SinoException("查询微信订单失败");
                }
            }
            return retVal;
        }
    }
    public static class StringExtension
    {
        

        public static string RSAWithPrivateKey(this string message,string certPath)
        {

            //byte[] keyData = Convert.FromBase64String(privateKey);
            //using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
            //using (RSACng rsa = new RSACng(cngKey))
            //{
            //    byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            //    return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
            //}
            return string.Empty;
            //try
            //{
            //    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            //    byte[] priKeyBytes = Convert.FromBase64String(privateKey);
            //    rsa.ImportCspBlob(priKeyBytes);
            //    byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            //    return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
            //}
            //catch
            //{
            //    byte[] keyData = Convert.FromBase64String(privateKey);
            //    using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
            //    using (RSACng rsa = new RSACng(cngKey))
            //    {
            //        byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            //        return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
            //    }
            //}
            //NLog.ILogger _logger = NLog.LogManager.GetCurrentClassLogger();
            //_logger.Info("进入RSA签名");

            //X509Certificate2 cer = new X509Certificate2(certPath);
            //if (cer != null)//获取公钥
            //{
            //    RSA rsa = cer.GetRSAPrivateKey();
            //    //查看在不同平台上的具体类型
            //    _logger.Info($"RSA类型:{rsa.GetType().FullName}");
            //    byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            //    return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
            //    //var isSig = pubkey.VerifyData(butys, CCB_ALG, verify);//验证信息
            //}
            //else
            //{
            //    throw new Exception("证书解析失败");
            //}
        }

        /// <summary>
        /// 微信RSA SHA256 签名
        /// </summary>
        /// <param name="message"></param>
        /// <param name="privateKey"></param>
        /// <returns></returns>
        public static string WXRSAWithSHA256Sign(this string message, string certPath, string certPwd)
        {
            //NLog.ILogger _logger = NLog.LogManager.GetCurrentClassLogger();
            X509Certificate2 cer = new X509Certificate2(certPath, certPwd, X509KeyStorageFlags.Exportable);
            if (cer != null)//获取公钥
            {
                RSA rsa = cer.GetRSAPrivateKey();
                //查看在不同平台上的具体类型
                //_logger.Info($"RSA类型:{rsa.GetType().FullName}");
                byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
                return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
            }
            else
            {
                throw new Exception("证书解析失败");
            }
        }
    }

    // 使用方法
    // HttpClient client = new HttpClient(new HttpHandler("{商户号}", "{商户证书序列号}"));
    // var response = client.GetAsync("https://api.mch.weixin.qq.com/v3/certificates");
    public class WXPayHttpHandler : DelegatingHandler
    {
        private readonly string merchantId;
        private readonly string serialNo;
        private readonly string certPath;
        private readonly string certPwd;
        private readonly string nonceStr;

        public WXPayHttpHandler(string merchantId, string merchantSerialNo, string certPath, string certPwd, string nonceStr)
        {
            InnerHandler = new HttpClientHandler();

            this.merchantId = merchantId;
            this.serialNo = merchantSerialNo;
            this.certPath = certPath;
            this.certPwd = certPwd;
            this.nonceStr = nonceStr;
        }

        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var auth = await BuildAuthAsync(request);
            string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";
            request.Headers.Add("Authorization", value);
            request.Headers.Add("Accept", "application/json");//如果缺少这句代码就会导致下单接口请求失败,报400错误(Bad Request)
            request.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");//如果缺少这句代码就会导致下单接口请求失败,报400错误(Bad Request)

            return await base.SendAsync(request, cancellationToken);
        }

        protected async Task<string> BuildAuthAsync(HttpRequestMessage request)
        {
            string method = request.Method.ToString();
            string body = "";
            if (method == "POST" || method == "PUT" || method == "PATCH")
            {
                var content = request.Content;
                body = await content.ReadAsStringAsync();//debug的时候在这里打个断点,看看body的值是多少,如果跟你传入的参数不一致,说明是有问题的,一定参考我的方法
            }

            string uri = request.RequestUri.PathAndQuery;
            var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
            string nonce = this.nonceStr;

            string message = $"{method}
{uri}
{timestamp}
{nonce}
{body}
";
            string signature = message.WXRSAWithSHA256Sign(this.certPath, this.certPwd);
            return $"mch>;
        }

        //protected string Sign(string message)
        //{
        //    // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----
        //    //        亦不包括结尾的-----END PRIVATE KEY-----
        //    string privateKey = this.privateKey;
        //    byte[] keyData = Convert.FromBase64String(privateKey);
        //    using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
        //    using (RSACng rsa = new RSACng(cngKey))
        //    {
        //        byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
        //        return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
        //    }
        //}
    }
    /// <summary>
    /// 应收微信支付配置
    /// </summary>
    public class ReceivablePayConfig
    {
        /// <summary>
        /// 主机地址
        /// </summary>
        public string Host { get; set; }

        /// <summary>
        /// 路由地址
        /// </summary>
        public string Route { get; set; }

        /// <summary>
        /// 生成二维码图片的像素大小 ,我这里设置的是5
        /// </summary>
        public int? PixelsPerModule { get; set; }

        /// <summary>
        /// 根据code获取微信授权的地址
        /// </summary>
        public string url { get; set; }

        /// <summary>
        /// appid
        /// </summary>
        public string appid { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string secret { get; set; }

        /// <summary>
        /// 授权方式
        /// </summary>
        public string grant_type { get; set; }

        /// <summary>
        /// 商户号
        /// </summary>
        public string mchid { get; set; }

        /// <summary>
        /// 商户序列号
        /// </summary>
        public string serial_no { get; set; }

        /// <summary>
        /// 回调通知地址
        /// </summary>
        public string notify_url { get; set; }

        /// <summary>
        /// 统一下单url
        /// </summary>
        public string UnifiedOrderUrl { get; set; }

        /// <summary>
        /// 商户订单号查询
        /// </summary>
        public string PayResultSearchUrl { get; set; }

        /// <summary>
        /// 微信支付私钥
        /// </summary>
        public string PrivateKey { get; set; }

        /// <summary>
        /// AES_KEY 微信支付回调使用
        /// </summary>
        public string AES_KEY { get; set; }

    }
    public class DependOutDataWXPay
    {
        /// <summary>
        /// 订单总金额,单位为分
        /// </summary>
        public int total { get; set; }

        /// <summary>
        /// 商户订单号    
        /// </summary>
        public string out_trade_no { get; set; }

        /// <summary>
        /// 商品描述
        /// </summary>
        public string description { get; set; }
    }

    public class PayOrderSearch
    {
        /// <summary>
        /// 商户订单号
        /// </summary>
        public string out_trade_no { get; set; }

    }

    /// <summary>
    /// 订单金额
    /// </summary>
    public class Amount
    {
        /// <summary>
        /// 订单总金额,单位为分
        /// </summary>
        public int total { get; set; }

        /// <summary>
        /// CNY:人民币,境内商户号仅支持人民币。
        /// </summary>
        public string currency { get; set; } = "CNY";
    }

    /// <summary>
    /// 支付者
    /// </summary>
    public class Payer
    {
        /// <summary>
        /// 用户在直连商户appid下的唯一标识。
        /// </summary>
        public string openid { get; set; }
    }

    /// <summary>
    /// 微信统一下单JSAPI(v3)
    /// </summary>
    public class WXUnifiedOrderReq
    {
        /// <summary>
        ///  直连商户申请的公众号appid
        /// </summary>
        public string appid { get; set; }

        /// <summary>
        /// 直连商户的商户号,由微信支付生成并下发
        /// </summary>
        public string mchid { get; set; }

        /// <summary>
        /// 商品描述
        /// </summary>
        public string description { get; set; }

        /// <summary>
        /// 商户订单号    
        /// </summary>
        public string out_trade_no { get; set; }

        /// <summary>
        /// 通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。
        /// </summary>
        public string notify_url { get; set; }

        /// <summary>
        /// 订单金额
        /// </summary>
        public Amount amount { get; set; }

        /// <summary>
        /// 支付者
        /// </summary>
        public Payer payer { get; set; }
    }
  public class WXCodeResult
    {
        [JsonProperty("access_token")]
        public string access_token { get; set; }

        [JsonProperty("expires_in")]
        public int expires_in { get; set; }

        [JsonProperty("refresh_token")]
        public string refresh_token { get; set; }

        [JsonProperty("openid")]
        public string openid { get; set; }

        [JsonProperty("scope")]
        public string scope { get; set; }
    }
   public class UnifiedOrderRes
    {
        public string prepay_id { get; set; }
    }
{
"ReceivablePayConfig": {
    "Host": "", /*跳转前端的域名*/
    "Route": "", /* 跳转前端的路由 */
    "PixelsPerModule": 5,
    "url": "https://api.weixin.qq.com/sns/oauth2/access_token?",
    "appid": "",
    "secret": "",
    "grant_type": "authorization_code",
    "mchid": "",
    /* 商户证书序列号 */
    "serial_no": "",
    "notify_url": "" /* 回调通知地址 */,
    "UnifiedOrderUrl": "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi" /* 统一下单url */,
    "PayResultSearchUrl": "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/", /* 商户订单号查询*/
    /* AES_KEY */
    "AES_KEY": "DCE2C90F34544BB1901DB0459033FBB0",
    /*微信支付私钥*/
    "PrivateKey": ""
  }
}