-->

Core3.1 微信v3 JSAPI支付

2021-01-22 18:06发布

1、前言

  “小魏呀,这个微信支付还要多久?”,“快了快了老板,就等着最后一步了。。。”,“搞快点哈,就等着上线呢”,“.........” 

因公司业务需要微信支付,以前没弄过花了几天时间写了一个微信v3的JSAPI支付,我滴个乖乖,差点今年小孩的奶粉就没了,还好弄出来了。在这里面各种踩坑,在这里记录一下,我开发的是微信公众号上面拉起微信支付。后台是Core3.1的接口,前端用的是Vue。后面是部署在CentOS上面的

2、写代码之前的准备

 你必须要有一个非个人性质的公众号(服务号),还有一个微信商户号。服务号申请地址,微信商户号申请地址,具体的根据网站申请中按人家要求来就行了。个人建议把申请下来的公众号里面的appid 、appsecret,微信商户平台,商户号等数据保存在数据库中。

3、公众号、商户号配置

    1)、公众号JS安全域名

登录公众号在左手边菜单:公众号设置---->功能设置------>JS安全域名----->设置。在里面可以连写5个域名下载文件上传到服务器上面 域名要经过ICP备案。可以访问到上面说的那个文件就可以了。

core3.1Api 发布后你放根目录是访问不到的,在configure里面加上访问静态文件 app.UseStaticFiles();然后在根目录建一个文件夹wwwroot 吧域名验证需要的txt文件丢进去  我是这么搞点。暂时没有想到其他骚操作

这里有人要问了 这个设置了是干嘛的,以前我也不知道是干嘛的哈哈,总有一颗好奇的心想知道。现在想想个人理解这个JS安全域名就是一个验证的机制吧。这里设置了加上微信服务号也有一个类似的,后面就可以调用JSAPI支付了。

    2)、网页授权域名

这个紧接着在JS安全域名后面  跟着设置一下就可以了 我部署在CentOS上面 看一下文件夹目录,还有一个文件夹里面是是p12文件 后面会提到

 

这个网页授权意思就是后面要获取到用户的OpenId的时候 要通过这个域名授权。我们就能获取到用户的信息,授权登录这些配置。后面图上还有一个HHhhjZj的文件这个是商户号上面设置的。

    3)、微信商户号设置

在微信商户平台上面选择产品中心---->开发配置,这里面设置支付目录。我这里是设置的一个 ,我也不是申请商户号的人 也没有这个权限 。上面的界面跟上面两步骤差不多就不啰嗦了。

 

   4)、微信商户号的key,V3key设置

这里不再重复 参考微信开发文档  微信JSAPI开发接入前准备 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml 慢慢来哦,设置秘钥 我倒是自己想着(阿猫阿狗888666)AMAG66688这种来拼够32位就可以了哈哈。

4、Core3.1后端代码 详解

前面这些弄好了只算搭好了环境 下面开始撸码。微信支付的逻辑就是,获取用户的OpenId------->统一下单获取payId-------------->拉起微信支付------------>支付回调接口写逻辑 

下面官网的

 

 

 

 

 

 

 

参考文档  JSAPI支付

   1)、封装微信请求类

这里我单独封装了一个微信支付的请求类。因为调用v3支付必须要符合APIV3接口规则 ,具体的在微信官方文档看

using App.Common.Base;using Microsoft.AspNetCore.Hosting;using Newtonsoft.Json;using System;using System.Collections.Generic;using System.IO;using System.Net;using System.Net.Http;using System.Net.Http.Headers;using System.Security.Cryptography;using System.Security.Cryptography.X509Certificates;using System.Text;using System.Threading;using System.Threading.Tasks;namespace App.Common.HttpHelper{ /// <summary> /// 请求类封装 别忘了 调用的时候添加 services.AddHttpClient() 调用都要加参数,实体类返回没有超时时间  /// 容器要添加 /// </summary> public class HttpClientFactoryHelper {  private readonly IHttpClientFactory _httpClientFactory;  private IWebHostEnvironment _webHostEnvironment;  public HttpClientFactoryHelper(IHttpClientFactory httpClientFactory, IWebHostEnvironment webHostEnvironment)  {   _httpClientFactory = httpClientFactory;   _webHostEnvironment = webHostEnvironment;  }    public void SaveLog(string text)  {   string thisTime = DateTime.Now.ToString("yyyyMMdd");   var path = _webHostEnvironment.ContentRootPath + $"/ApiInterfaceErrorLog/";//绝对路径   string dirPath = Path.Combine(path, thisTime + "/");//绝对径路 储存文件路径的文件夹   if (!Directory.Exists(dirPath))//查看文件夹是否存在    Directory.CreateDirectory(dirPath);   string splitLine = "============================下一条==============================";   string timeNow = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");   using StreamWriter file = new StreamWriter(dirPath + thisTime + ".txt", true);   file.WriteLine(timeNow+text);   file.WriteLine(splitLine);  }  #region //微信支付请求  /// <summary>  /// 微信请求Post  /// </summary>  /// <param name="url">地址</param>  /// <param name="requestString">参数</param>  /// <param name="privateKey">私有秘钥 p12文件</param>  /// <param name="merchantId">商户号</param>  /// <param name="serialNo">商户证书号</param>  /// <returns></returns>  public async Task<string> WeChatPostAsync(string url,string requestString, string privateKey, string merchantId, string serialNo)  {   try   {    var client = _httpClientFactory.CreateClient();    var requestContent = new StringContent(requestString);    requestContent.Headers.ContentType.MediaType = "application/json";    var auth = BuildAuthAsync(url, requestString, privateKey, merchantId, serialNo,"POST");    string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";    client.DefaultRequestHeaders.Add("Authorization", value);    client.DefaultRequestHeaders.Add("Accept", "application/json");    client.DefaultRequestHeaders.Add("User-Agent", "WOW64");    client.Timeout = TimeSpan.FromSeconds(60);    var response = await client.PostAsync(url, requestContent);    if (response.IsSuccessStatusCode)    {     var result = await response.Content.ReadAsStringAsync();     return result;    }    else    {     return $"接口【{url}】请求错误,错误代码{response.StatusCode},错误原因{response.ReasonPhrase}具体的话========================================\n{JsonConvert.SerializeObject(response)}";    }   }   catch(Exception ex)   {    SaveLog($"接口【{DateTime.Now +url}】请求错误,错误代码{ex.Message}具体=============================================/n{ex.StackTrace}");    return ex.Message + ex.StackTrace;   }  }  /// <summary>  /// 微信请求  /// </summary>  /// <param name="url">地址</param>  /// <param name="requestString">参数</param>  /// <param name="privateKey">私有秘钥 p12文件</param>  /// <param name="merchantId">商户号</param>  /// <param name="serialNo">商户证书号</param>  /// <param name="method">Get或者Post</param>  /// <returns></returns>  public async Task<string> WeChatGetAsync(string url, string privateKey, string merchantId, string serialNo)  {   try   {    var client = _httpClientFactory.CreateClient();    var auth = BuildAuthAsync(url, "", privateKey, merchantId, serialNo,"GET");    string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";    client.DefaultRequestHeaders.Add("Authorization", value);    client.DefaultRequestHeaders.Add("Accept", "*/*");    client.DefaultRequestHeaders.Add("User-Agent", "WOW64");    client.Timeout = TimeSpan.FromSeconds(60);    var response = await client.GetAsync(url);    if (response.IsSuccessStatusCode)    {     var result = await response.Content.ReadAsStringAsync();     return result;    }    else    {     return $"接口【{url}】请求错误,错误代码{response.StatusCode},错误原因{response.ReasonPhrase}";    }   }   catch (Exception ex)   {    SaveLog($"接口【{DateTime.Now + url}】请求错误,错误代码{ex.Message}具体=============================================/n{ex.StackTrace}");    return ex.Message+ ex.StackTrace;   }  }  protected string BuildAuthAsync(string url,string jsonParame ,string privateKey, string merchantId,string serialNo,string method="")  {   string body = jsonParame;   var uri = new Uri(url);   var urlPath = uri.PathAndQuery;   var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();   string nonce = Path.GetRandomFileName();   string message = $"{method}\n{urlPath}\n{timestamp}\n{nonce}\n{body}\n";   //string signature = Sign(message, privateKey);   var path = _webHostEnvironment.WebRootPath + "/arsjkll/apiclient_cert.p12";   string signature = Sign(message,path, "1601849569");   return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\"";  }  /// <summary>  /// 签名(CentOs 不支持 换了下面的)  /// </summary>  /// <param name="message">签名内容</param>  /// <param name="privateKey">秘钥</param>  /// <returns></returns>  public string Sign(string message,string 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>  /// <param name="priKeyFile">证书文件路径</param>  /// <param name="keyPwd">密码</param>  /// <returns></returns>  private static RSA GetPrivateKey(string priKeyFile, string keyPwd)  {   var pc = new X509Certificate2(priKeyFile, keyPwd, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);   return (RSA)pc.PrivateKey;  }  /// <summary>  //// <summary>  /// 根据证书签名数据 后面要做成配置在数据库中  /// </summary>  /// <param name="data">要签名的数据</param>  /// <param name="certPah">证书路径</param>  /// <param name="certPwd">密码</param>  /// <returns></returns>  public string Sign(string data, string certPah, string certPwd)  {   var rsa = GetPrivateKey(certPah, certPwd);   var rsaClear = new RSACryptoServiceProvider();   var paras = rsa.ExportParameters(true);   rsaClear.ImportParameters(paras);   var signData = rsa.SignData(Encoding.UTF8.GetBytes(data), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);   return Convert.ToBase64String(signData);  }  #endregion } public class CustomerHttpException : Exception {  public CustomerHttpException() : base()  { }  public CustomerHttpException(string message) : base(message)  {  } } public class ReturnData {  /// <summary>  /// 返回码  /// </summary>  public int Code { get; set; }  /// <summary>  /// 消息  /// </summary>  public string Msg { get; set; }  /// <summary>  /// 是否成功  /// </summary>  public bool IsSuccess { get; set; }  /// <summary>  /// 返回数据  /// </summary>  public object Data { get; set; }  /// <summary>  /// 成功默认  /// </summary>  /// <param name="data"></param>  /// <param name="msg"></param>  public static ReturnData ToSuccess(object data, string msg = "sussess")  {   var result = new ReturnData   {    Code = (int)HttpStatusCode.OK,    IsSuccess = true,    Msg = msg,    Data = data   };   return result;  }  public static ReturnData ToFail(string msg)  {   var result = new ReturnData   {    Code = (int)HttpStatusCode.BadRequest,    IsSuccess = false,    Msg = msg   };   return result;  }  /// <summary>  /// 异常  /// </summary>  /// <param name="msg"></param>  /// <param name="data"></param>  public static ReturnData ToError(Exception ex, object data = null)  {   var result = new ReturnData   {    Code = (int)HttpStatusCode.InternalServerError,    IsSuccess = false,    Msg = "异常" + ex.Message,    Data = data   };   return result;  }  /// <summary>  /// 未经授权  /// </summary>  /// <param name="data"></param>  public static ReturnData ToNoToken(object data = null)  {   var result = new ReturnData   {    Code = (int)HttpStatusCode.Forbidden,    IsSuccess = false,    Msg = "未经授权不能访问",    Data = data   };   return result;  }  public static ReturnData Instance(object data, string msg, int code)  {   var result = new ReturnData   {    Code = code,    IsSuccess = false,    Msg = msg,    Data = data   };   return result;  } }}

Sign签名官方给的只能在IIS上面运行 那是通过直接用私钥签名,我在CentOS上面不行

 以前在IIS上面也是 但是这个只要配置IIS应用程序池,把加载用户配置文件改成true就可以了。CentOS上面就不行了。后来我还是把p12文件放在了跟验证域名的那个位置,通过读取文件获取私钥。这个问题搞了我2天。。。不能跨平台。或者是我配置不对,后面有时间在研究。

    2)、获取用户的OpenId 

在用户统一下单的时候需要用户的OpenId就是这个用户在这个公众号下面的一个身份号码,关没关注获取了就不会变,所以我就是没调用统一下单之前就获取了保存在数据库中。统一下单的时候直接调用就可以了 参考连接  公众号网页授权

private const string AuthorUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?";  private const string Oauth = "https://api.weixin.qq.com/sns/oauth2/access_token?";  private const string GetUserInfo = "https://api.w.........
标签: