使用C#版第三方微信SDK集成微信支付

使用C#版第三方微信SDK集成微信支付

概述

最近在工作中要开发一个微信支付的功能,项目比较急,第一反应就是去官方文档找SDK,发现官方只提供了JAVA、PHP、Go的SDK。(对C#玩家真不友好!=_=!)。好在微信支付的开发者社区有位大哥fudiwei (RHQYZ) (github.com)提供了C#版的SDK,项目地址:DotNetCore.SKIT.FlurlHttpClient.Wechat,果断Install一下。

SDK简介

SKIT.FlurlHttpClient.Wechat是基于 Flurl.Http 的微信 HTTP API SDK,目前已包含公众平台、开放平台、商户平台、企业微信、广告平台、对话开放平台等模块。

看了一下文档,已经把微信已知的API都封装了,而且更新的还比较快,github上作者Issues回复也很快,还是比较稳定,完全够用啦。

接入使用

我这里只用到了JSAPI支付,其他的需求可查看项目文档,都有详细介绍。

准备工作

  • 申请商户号
  • 开启V3 api权限
  • 下载密钥和序列号

配置文件

我是新建了一个叫tenpay_setting.json的配置文件,也可以直接加到appsetting.json

{
  "Tenpay": {
    "Merchants": [
      {
        "MerchantId": "********", //商户号
        "SecretV3": "*********", //V3版本Api的Secret
        "CertificateSerialNumber": "*********",//证书序列号
        "CertificatePrivateKey": "apiclient_key.pem"//api key 可以直接将内容复制上来,我嫌太长,用的路径
      }
    ],
    "NotifyUrl": "http://www.host.com/api/notify/wx/{merchant_id}/pay"
  }
}

请求客户端创建

项目文档上是在多租户的情况下使用factory创建,我这里没有分租户。

/// <summary>
/// 创建微信支付客户端
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public WechatTenpayClient CreateClient(string merchantId)
{
    var tenpayMerchantConfig = GetDefaultMerchant(merchantId);

    var options = new WechatTenpayClientOptions()
    {
        //商户号
        MerchantId = tenpayMerchantConfig.MerchantId,
        //v3Secret
        MerchantV3Secret = tenpayMerchantConfig.SecretV3,
        //证书序列号
        MerchantCertificateSerialNumber = tenpayMerchantConfig.CertificateSerialNumber,
        //私钥
        MerchantCertificatePrivateKey = GetCertPrivateKey(tenpayMerchantConfig.CertificatePrivateKey),
        //证书管理器
        PlatformCertificateManager = _redisCertificateManager,
        //开启自动加密
        AutoEncryptRequestSensitiveProperty = true,
        //开启自动解密
        AutoDecryptResponseSensitiveProperty = true
    };
    var wechatTenpayClient = WechatTenpayClientBuilder.Create(options).Build();
    return wechatTenpayClient;
}

读取私钥内容

/// <summary>
/// 读取私钥内容
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <exception cref="BizException"></exception>
private static string GetCertPrivateKey(string path)
{
    try
    {
        // 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----
        // 亦不包括结尾的         -----END PRIVATE KEY-----
        var certPath = path;
        var certPrivateKey = File.ReadAllText(certPath);
        //以前的版本需要移出这两行,现在好像不需要了
        //certPrivateKey = certPrivateKey.Replace("-----BEGIN PRIVATE KEY-----", string.Empty);
        //certPrivateKey = certPrivateKey.Replace("-----END PRIVATE KEY-----", string.Empty);
        //certPrivateKey = certPrivateKey.Trim();
        return certPrivateKey;
    }
    catch (Exception ex)
    {
        throw new BizException($"获取证书内容失败!:{ex.Message}");
    }
}

注意,新版本中不需要处理-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----,否则会出现Private key format is not supported.

自动刷新证书

项目simple中是使用内存管理证书,我这项目需要其他微服务使用,就用redis缓存管理

/// <summary>
/// redis缓存管理证书
/// </summary>
public class RedisCertificateManager : ICertificateManager, ITransient
{
    private const string REDIS_KEY_PREFIX = "wxpaypc-";
    private ICacheManager _cache;

    /// <summary>
    /// 构造
    /// </summary>
    /// <param name="cacheManager"></param>
    public RedisCertificateManager(ICacheManager cacheManager)
    {
        _cache = cacheManager;
    }

    private string GenerateRedisKey(string serialNumber)
    {
        return $"{REDIS_KEY_PREFIX}{serialNumber}";
    }

    /// <summary>
    /// 获得所有证书
    /// </summary>
    /// <returns></returns>
    public IEnumerable<CertificateEntry> AllEntries()
    {

        var certs = _cache.GetByPrefix<CertificateEntry>($"{REDIS_KEY_PREFIX}*") ?? new Dictionary<string, CertificateEntry>();
        if (certs.Any())
        {
            return certs
                .Select(t => t.Value)
                .ToArray();
        }

        return Array.Empty<CertificateEntry>();
    }

    /// <summary>
    /// 添加证书
    /// </summary>
    /// <param name="entry"></param>
    public void AddEntry(CertificateEntry entry)
    {
        string key = GenerateRedisKey(entry.SerialNumber);
        _cache.Set(key, entry);
    }

    /// <summary>
    /// 获得最新的证书
    /// </summary>
    /// <param name="serialNumber"></param>
    /// <returns></returns>
    public CertificateEntry? GetEntry(string serialNumber)
    {
        string key = GenerateRedisKey(serialNumber);
        var values = AllEntries();
        if (values.Any())
        {
            return values.OrderByDescending(a => a.ExpireTime).FirstOrDefault();
        }

        return null;
    }

    /// <summary>
    /// 移出一个证书
    /// </summary>
    /// <param name="serialNumber"></param>
    /// <returns></returns>
    public bool RemoveEntry(string serialNumber)
    {
        string key = GenerateRedisKey(serialNumber);
        _cache.Remove(key);
        return true;
    }
}

Worker干的事,为了以后可能出现多商户,也搞了循环商户数组

while (!stoppingToken.IsCancellationRequested)
{
    foreach (var tenpayMerchantOptions in _tenpayOptions.Merchants)
    {
        try
        {
            const string ALGORITHM_TYPE = "RSA";
            var client = _tenpayService.CreateClient(tenpayMerchantOptions.MerchantId);
            var request = new QueryCertificatesRequest() { AlgorithmType = ALGORITHM_TYPE };
            var response = await client.ExecuteQueryCertificatesAsync(request, cancellationToken: stoppingToken);
            if (response.IsSuccessful())
            {
                // NOTICE:
                //   如果构造 Client 时启用了 `AutoDecryptResponseSensitiveProperty` 配置项,则无需再执行下面一行的手动解密方法:
                //response = client.DecryptResponseSensitiveProperty(response);

                foreach (var certificate in response.CertificateList)
                {
                    client.PlatformCertificateManager.AddEntry(CertificateEntry.Parse(ALGORITHM_TYPE, certificate));
                }

                _logger.LogInformation("刷新微信商户平台证书成功。");
            }
            else
            {
                _logger.LogWarning(
                    "刷新微信商户平台证书失败(状态码:{0},错误代码:{1},错误描述:{2})。",
                    response.GetRawStatus(), response.ErrorCode, response.ErrorMessage
                );
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "刷新微信商户平台证书遇到异常。");
        }
    }

    await Task.Delay(TimeSpan.FromDays(1), stoppingToken); // 每隔 1 天轮询刷新
}

创建订单

/// <summary>
/// 通过Jsapi创建订单
/// </summary>
/// <param name="requestModel"></param>
/// <returns></returns>
public async Task<CreatePayTransactionJsapiResponse> CreateOrderByJsapi(CreateOrderByJsapiRequest requestModel)
{
    var client = CreateClient(requestModel.MerchantId);

    var notifyUrl = _tenpayOptions.NotifyUrl;
    if (notifyUrl.Contains("{merchant_id}"))
        notifyUrl = notifyUrl.Replace("{merchant_id}", requestModel.MerchantId);

    var amount = Convert.ToInt32(requestModel.Amount * 100);
    var request = new CreatePayTransactionJsapiRequest()
    {
        OutTradeNumber = requestModel.OutTradeNumber,
        AppId = requestModel.AppId,
        Description = requestModel.Description,
        NotifyUrl = notifyUrl,
        Amount = new CreatePayTransactionJsapiRequest.Types.Amount() { Total = amount },
        Payer = new CreatePayTransactionJsapiRequest.Types.Payer() { OpenId = requestModel.OpenId }
    };
    CreatePayTransactionJsapiResponse response = await client.ExecuteCreatePayTransactionJsapiAsync(request);
    if (!response.IsSuccessful())
    {
        throw new(
            $"JSAPI 下单失败(状态码:{response.GetRawStatus()},错误代码:{response.ErrorCode},错误描述:{response.ErrorMessage})。"
        );
    }
    return response;
}

生成客户端参数

拿着下单后的PrepayId就可以去获取客户端所需参数了

/// <summary>
/// 生成客户端 JSAPI / 小程序调起支付所需的参数字典
/// </summary>
/// <returns></returns>
public IDictionary<string, string> GetParametersForJsapiPayRequest(ParametersForJsapiPayRequest requestModel)
{
    try
    {
        // 创建客户端
        var client = CreateClient(requestModel.MerchantId);
        var res = client.GenerateParametersForJsapiPayRequest(requestModel.AppId, requestModel.PrepayId);
        var ret = new Dictionary<string, string>(res)
        {
            // 加上生成的业务单号,方便查询订单
            { "billno", requestModel.BillNo }
        };
        return ret;
    }
    catch (Exception ex)
    {
        throw new("生成客户端 JSAPI / 小程序调起支付所需的参数字典异常", ex);
    }

}

回调方法

/// <summary>
/// 微信支付回调
/// </summary>
/// <param name="merchantId"></param>
/// <param name="timestamp"></param>
/// <param name="nonce"></param>
/// <param name="signature"></param>
/// <param name="serialNumber"></param>
/// <returns></returns>
[HttpPost]
[Route("wx/{merchant_id}/pay")]
public async Task<IActionResult> ReceiveMessage(
    [FromRoute(Name = "merchant_id")] string merchantId,
    [FromHeader(Name = "Wechatpay-Timestamp")] string timestamp,
    [FromHeader(Name = "Wechatpay-Nonce")] string nonce,
    [FromHeader(Name = "Wechatpay-Signature")] string signature,
    [FromHeader(Name = "Wechatpay-Serial")] string serialNumber)
{
    using var reader = new StreamReader(App.HttpContext.Request.Body, Encoding.UTF8);
    string content = await reader.ReadToEndAsync();
    //_logger.LogInformation("接收到微信支付推送的数据:{0}", content);

    var client = _tenPayService.CreateClient(merchantId);
    bool valid = client.VerifyEventSignature(
        webhookTimestamp: timestamp,
        webhookNonce: nonce,
        webhookBody: content,
        webhookSignature: signature,
        webhookSerialNumber: serialNumber
    );
    if (!valid)
    {
        // NOTICE:
        //   需提前注入 CertificateManager、并下载平台证书,才可以使用扩展方法执行验签操作。
        //   请参考本示例项目 TenpayCertificateRefreshingBackgroundService 后台任务中的相关实现。
        //   有关 CertificateManager 的完整介绍请参阅《开发文档 / 基础用法 / 如何验证回调通知事件签名?》。
        //   后续如何解密并反序列化,请参阅《开发文档 / 基础用法 / 如何解密回调通知事件中的敏感数据?》。

        //记录验签失败日志
        await ExceptionLogHelper.WriteRequestToLogAsync(_logger, App.HttpContext, new Exception("微信支付验签失败"), App.Configuration);
        return new JsonResult(new { code = "FAIL", message = "验签失败" });
    }

    WechatTenpayEvent callbackModel = client.DeserializeEvent(content);
    var eventType = callbackModel.EventType?.ToUpper();
    //解密数据
    TransactionResource callbackResource = client.DecryptEventResource<TransactionResource>(callbackModel);
    switch (eventType)
    {
        case "TRANSACTION.SUCCESS":
            {

                //_logger.LogInformation("接收到微信支付推送的订单支付成功通知,商户订单号:{0}", callbackResource.OutTradeNumber);
                // 成功情况
                _vipService.DoPaySuccessOp(callbackResource, callbackModel);
            }
            break;

        default:
            {
                if (callbackResource.IsNull())
                {
                    //记录其他情况日志
                    await ExceptionLogHelper.WriteRequestToLogAsync(_logger, App.HttpContext, new Exception("微信支付通知失败"), App.Configuration);
                }

                // 其他情况默认失败
                await _vipService.DoPayFailureOpAsync($"回调发生错误:{callbackModel.Summary}", callbackResource.OutTradeNumber, callbackModel.CreateTime.ToDate(), JToken.FromObject(callbackModel));
            }
            break;
    }

    return new JsonResult(new { code = "SUCCESS", message = "成功" });
}

至此就ok啦,业务处理的代码就不放了,根据自己的情况编写。

其他

前端

本地测试时,支付用的时wx官方提供的小程序示例,其中有个模块叫接口能力——发起支付,改改提交方法即可

wx.requestPayment
(
  {
    "appId": "**********",
    "timeStamp": "1708416150",
    "nonceStr": ""**********",",
    "package": "prepay_id="**********",",
    "signType": "RSA",
    "paySign": ""**********",",
    "billno": "cz_517123880726597",
    "success":function(res){},
    "fail":function(res){},
    "complete":function(res){}
  }
)

回调测试

如果没有测试服务器,像我一样,可以使用natapp内网穿透,如果在公司测试的话开个内网穿透就ok了

如果有其他啥好方法也可以分享一下!

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值