You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
12 KiB

2 years ago
#region Apache License Version 2.0
/*----------------------------------------------------------------
Copyright 2023 Jeffrey Su & Suzhou Senparc Network Technology Co.,Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the specific language governing permissions
and limitations under the License.
Detail: https://github.com/JeffreySu/WeiXinMPSDK/blob/master/license.md
----------------------------------------------------------------*/
#endregion Apache License Version 2.0
/*----------------------------------------------------------------
Copyright (C) 2022 Senparc
TenPayHttpClient.cs
TenPayHttpClient
Senparc - 20210920
Senparc - 20220511
v0.6.2.2 TenPayHttpClient
----------------------------------------------------------------*/
using Client.TenPayHttpClient.Signer;
using Senparc.CO2NET;
using Senparc.CO2NET.Extensions;
using Senparc.CO2NET.Helpers;
using Senparc.CO2NET.HttpUtility;
using Senparc.CO2NET.Trace;
using Senparc.Weixin;
using Senparc.Weixin.Entities;
using Senparc.Weixin.TenPayV3;
using Senparc.Weixin.TenPayV3.Apis.BasePay;
using Senparc.Weixin.TenPayV3.Apis.Entities;
using Senparc.Weixin.TenPayV3.TenPayHttpClient.Verifier;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Senparc.Weixin.TenPayV3.TenPayHttpClient
{
public class BasePayApis2
{
private readonly SenparcHttpClient _httpClient;
private ISenparcWeixinSettingForTenpayV3 _tenpayV3Setting;
private readonly CertType _certType;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="senparcWeixinSettingForTenpayV3"></param>
public BasePayApis2(SenparcHttpClient httpClient, ISenparcWeixinSettingForTenpayV3 senparcWeixinSettingForTenpayV3 = null, CertType certType = CertType.RSA)
{
this._httpClient = httpClient;
_tenpayV3Setting = senparcWeixinSettingForTenpayV3 ?? Senparc.Weixin.Config.SenparcWeixinSetting.TenpayV3Setting;
this._certType = certType;
}
/// <summary>
/// 返回可用的微信支付地址(自动判断是否使用沙箱)
/// </summary>
/// <param name="urlFormat">如:<code>https://api.mch.weixin.qq.com/{0}pay/unifiedorder</code></param>
/// <returns></returns>
private static string ReturnPayApiUrl(string urlFormat)
{
//注意:目前微信支付 V3 还没有支持沙箱,此处只是预留
return string.Format(urlFormat, Senparc.Weixin.Config.UseSandBoxPay ? "sandboxnew/" : "");
}
/// <summary>
/// JSAPI下单接口
/// <para>在微信支付服务后台生成JSAPI预支付交易单返回预支付交易会话标识</para>
/// </summary>
/// <param name="data">微信支付需要POST的Data数据</param>
/// <param name="timeOut">超时时间单位为ms </param>
/// <returns></returns>
public async Task<JsApiReturnJson> JsApiAsync(TransactionsRequestData data, int timeOut = Config.TIME_OUT)
{
var url = ReturnPayApiUrl(Senparc.Weixin.Config.TenPayV3Host + "/{0}v3/pay/transactions/jsapi");
HttpClient hc = null;//注入
TenPayHttpClient tenPayApiRequest = new(_httpClient, _tenpayV3Setting);
return await tenPayApiRequest.SendAsync<JsApiReturnJson>(url, data, timeOut);
}
}
public class TenPayHttpClient
{
private readonly SenparcHttpClient _httpClient;
private ISenparcWeixinSettingForTenpayV3 _tenpayV3Setting;
private readonly HttpClient _client;
private readonly ISigner _signer;
private readonly IVerifier _verifier;
public TenPayHttpClient(SenparcHttpClient httpClient, ISenparcWeixinSettingForTenpayV3 senparcWeixinSettingForTenpayV3 = null, CertType certType = CertType.RSA)
{
this._httpClient = httpClient;
this._client = this._httpClient.Client;
_tenpayV3Setting = senparcWeixinSettingForTenpayV3 ?? Senparc.Weixin.Config.SenparcWeixinSetting.TenpayV3Setting;
//从工厂获得签名和验签的方法类
_signer = TenPayCertFactory.GetSigner(certType);
_verifier = TenPayCertFactory.GetVerifier(certType);
#region 配置UA
//ACCEPT header
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
//User-Agent header
var userAgentValues = UserAgentValues.Instance;
_client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Senparc.Weixin.TenPayV3-C#", userAgentValues.TenPayV3Version));
_client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"(Senparc.Weixin {userAgentValues.SenparcWeixinVersion})"));
_client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(".NET", userAgentValues.RuntimeVersion));
_client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({userAgentValues.OSVersion})"));
#endregion
}
public async Task<T> SendAsync<T>(string url, object data, int timeOut = Senparc.Weixin.Config.TIME_OUT, ApiRequestMethod requestMethod = ApiRequestMethod.POST, bool checkSign = true, Func<T> createDefaultInstance = null)
where T : ReturnJsonBase/*, new()*/
{
T result = null;
HttpMethod method = GetHttpMethod(requestMethod);
try
{
var request = new HttpRequestMessage(method, url);
//设置超时时间
_client.Timeout = TimeSpan.FromMilliseconds(timeOut);
//设置请求 Json 字符串
string jsonString = data != null
? data.ToJson(false, new Newtonsoft.Json.JsonSerializerSettings() { NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore })
: "";
WeixinTrace.SendApiPostDataLog(url, jsonString); //记录Post的Json数据
request.Content = new StringContent(jsonString, Encoding.UTF8, mediaType: "application/json");
// 进行签名
var authorization = await GenerateAuthorizationHeader(request);
request.Headers.Add("Authorization", $"WECHATPAY2-{_signer.GetAlgorithm()} {authorization}");
// 发送请求
var responseMessage = await _client.SendAsync(request);
//获取响应结果
string content = await responseMessage.Content.ReadAsStringAsync();//TODO:如果不正确也要返回详情
#if DEBUG
Console.WriteLine("Content:" + content + ",,Headers:" + responseMessage.Headers.ToString());
#endif
//检查响应代码
TenPayApiResultCode resutlCode = TenPayApiResultCode.TryGetCode(responseMessage.StatusCode, content);
if (resutlCode.Success)
{
result = content.GetObject<T>();
if (checkSign)
{
result.VerifySignSuccess = await VerifyResponseMessage(responseMessage, content);
}
}
else
{
result = createDefaultInstance?.Invoke() ?? GetInstance<T>(true);
resutlCode.Additional = content;
}
result.ResultCode = resutlCode;
return result;
}
catch (Exception ex)
{
SenparcTrace.BaseExceptionLog(ex);
result = createDefaultInstance?.Invoke() ?? GetInstance<T>(false);
if (result != null)
{
result.ResultCode = new() { ErrorMessage = ex.Message };
}
return result;
}
}
protected HttpMethod GetHttpMethod(ApiRequestMethod requestMethod)
{
return requestMethod switch
{
ApiRequestMethod.GET => HttpMethod.Get,
ApiRequestMethod.POST => HttpMethod.Post,
ApiRequestMethod.PUT => HttpMethod.Put,
ApiRequestMethod.PATCH => HttpMethod.Patch,
ApiRequestMethod.DELETE => HttpMethod.Delete,
_ => throw new ArgumentOutOfRangeException(nameof(requestMethod)),
};
}
protected async Task<string> GenerateAuthorizationHeader(HttpRequestMessage request)
{
string method = request.Method.ToString();
string body = "";
if (method == "POST" || method == "PUT" || method == "PATCH")
{
var content = request.Content;
body = await content.ReadAsStringAsync();
}
string uri = request.RequestUri.PathAndQuery;
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = Path.GetRandomFileName();
string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";
string signature = _signer.Sign(message, _tenpayV3Setting.TenPayV3_PrivateKey);
return $"mchid=\"{_tenpayV3Setting.TenPayV3_MchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_tenpayV3Setting.TenPayV3_SerialNumber}\",signature=\"{signature}\"";
}
protected async Task<bool> VerifyResponseMessage(HttpResponseMessage responseMessage, string content)
{
var wechatpayTimestamp = responseMessage.Headers.GetValues("Wechatpay-Timestamp").First();
var wechatpayNonce = responseMessage.Headers.GetValues("Wechatpay-Nonce").First();
var wechatpaySignatureBase64 = responseMessage.Headers.GetValues("Wechatpay-Signature").First();//后续需要base64解码
var wechatpaySerial = responseMessage.Headers.GetValues("Wechatpay-Serial").First();
try
{
var pubKey = await TenPayV3InfoCollection.GetAPIv3PublicKeyAsync(this._tenpayV3Setting, wechatpaySerial);
return _verifier.Verify(wechatpayTimestamp, wechatpayNonce, wechatpaySignatureBase64, content, pubKey);
}
catch (Exception ex)
{
throw new TenpayApiRequestException("RequestAsync 签名验证失败:" + ex.Message, ex);
}
}
/// <summary>
/// 获取实例
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="throwIfFaild"></param>
/// <returns></returns>
private T GetInstance<T>(bool throwIfFaild)
where T : ReturnJsonBase
{
if (typeof(T).IsClass)
{
return Senparc.CO2NET.Helpers.ReflectionHelper.CreateInstance<T>(typeof(T).FullName, typeof(T).Assembly.GetName().Name);
}
else if (throwIfFaild)
{
throw new TenpayApiRequestException("GetInstance 失败,此类型无法自动生成:" + typeof(T).FullName);
}
return null;
}
}
}