评论

ASP.NET Core 微信支付

本地调试时记得安装微信支付安全证书!!!发布到服务器上也要安装微信支付安全证书!!!私钥从微信支付后台发放的证书中拷贝出来!!!!!

前言

		前几个周整理了微信支付的统一下单、支付结果通知、订单查询接口。趁这几天有时间,整理了一下我的代码,又把APP支付的退款接口、关闭订单整理了,把APP支付的后端接口整理了一下。

		github仓库地址:[https://github.com/wolfsheep/WeChatAPIv3ForAppWithNetCore](https://github.com/wolfsheep/WeChatAPIv3ForAppWithNetCore)

		gitee仓库地址:[https://gitee.com/dawenyang_admin/WeChatAPIv3ForAppWithNetCore](https://gitee.com/dawenyang_admin/WeChatAPIv3ForAppWithNetCore)

		把我的代码重构了一遍,更方便大家的使用。可能还有很多问题,欢迎大家反馈。下面我具体列一下我的实现。

重要的事说在前面

  1. 本地调试时记得安装微信支付安全证书!!!
  2. 发布到服务器上也要安装微信支付安全证书!!!
  3. 私钥从微信支付后台发放的证书中拷贝出来!!!!!

理论实战

统一下单

调用方法


        private readonly ILogger<WeChatPayTestController> _logger;

        private readonly IConfiguration _configuration;



        public WeChatPayTestController(ILogger<WeChatPayTestController> logger, IConfiguration configuration)

        {

            _logger = logger;

            _configuration = configuration;

        }

        /// <summary>

        /// 统一下单接口

        /// </summary>

        /// <returns></returns>

        [HttpGet, Route("api/WeChatPayTest/GenerateOrder")]

        public async Task<AppPayModel> GenerateOrder()

        {

            var orderNumber = $"{DateTime.Now:yyyyMMddHHmmssff}{CodeHelper.CreateNumCode(3)}";

            var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);

            var notify_url = _configuration["notify_url"]; //这个放在配置文件,从配置文件读取比较灵活,或者写到数据库中

            var payodel = await helper.UnionGenerateOrder("好东西啊", 1, orderNumber, notify_url, "附加信息测试啊");

            #region 为APP生成下单所需的参数,看个人实际需求,也可以APP自己生成所需的参数

            var signModel = WxPayForAppHelper.GetSign(WxPayConst.appid, payodel.prepay_id, WxPayConst.privateKey);

            #endregion

            return signModel;

        }

查询订单

调用方法


        /// <summary>

        /// 查询订单接口 

        /// </summary>

        [HttpGet, Route("api/WeChatPayTest/QueryOrder")]

        public async Task<WxPayStatusRespModel> QueryOrder(string orderNumber)

        {

            var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);

            var payModel = await helper.QueryOrder(orderNumber);

            return payModel;

        }

关闭订单

调用方法


        /// <summary>

        /// 查询订单接口

        /// </summary>

        [HttpGet, Route("api/WeChatPayTest/CloseOrder")]

        public async Task<ClostOrderRespModel> CloseOrder(string orderNumber)

        {

            var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);

            var payModel = await helper.ClostOrder(orderNumber);

            return payModel;

        }

支付结果通知

调用方法

    /// <summary>

    /// 微信支付成功结果回调接口

    /// </summary>

    /// <returns>退款通知http应答码为200且返回状态码为SUCCESS才会当做商户接收成功,否则会重试。注意:重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。</returns>

    [HttpPost, Route("api/WeChatPayTest/WxPayCallback")]

    [AllowAnonymous]

    public async Task<WxPayCallbackRespModel> WxPayCallback()

    {

        #region 获取字符串流

        /**

         * .NET 获取字符串流

         *  System.IO.Stream s = HttpContext.Current.Request.InputStream;

            int count = 0;

            byte[] buffer = new byte[1024];

            StringBuilder builder = new StringBuilder();

            while ((count = s.Read(buffer, 0, 1024)) > 0)

            {

                builder.Append(Encoding.UTF8.GetString(buffer, 0, count));

            }

            s.Flush();

            s.Close();

            s.Dispose();

         */

        var buffer = new MemoryStream();

        Request.Body.CopyTo(buffer);

        #endregion

        //我没有使用官方的那种验证数据安全性的方法,我解密出来数据之后,直接拿着订单号再去查询一下订单状态,然后再更新到数据库中。我嫌麻烦……

        var str = Encoding.UTF8.GetString(buffer.GetBuffer());

        var wxPayNotifyModel = str.ToObject<WxPayNotifyModel>();

        var resource = wxPayNotifyModel?.resource ?? new WxPayResourceModel();

        var decryptStr = AesGcmHelper.AesGcmDecrypt(resource.associated_data, resource.nonce, resource.ciphertext, WxPayConst.APIV3Key);

        var decryptModel = decryptStr.ToObject<WxPayResourceDecryptModel>();

        var viewModel = new WxPayCallbackRespModel();

        if (string.IsNullOrEmpty(decryptModel.out_trade_no))

        {

            viewModel.code = "FAIL";

            viewModel.message = "数据解密失败";

        }

        else

        {

            var resp = await QueryOrder(decryptModel.out_trade_no);

            //然后进行数据库更新处理……等等其他操作

        }

        return viewModel;

    }

申请退款

调用方法


        /// <summary>

        /// 退款接口 

        /// </summary>

        [HttpGet, Route("api/WeChatPayTest/Refunds")]

        public async Task<RefundsRespModel> Refunds(string orderNumber)

        {

            var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);

            var refundNumber = $"{DateTime.Now:yyyyMMddHHmmssff}{CodeHelper.CreateNumCode(3)}";

            var payModel = await helper.Refunds(orderNumber, refundNumber, "测试退款行不行", 1, 2, "https://xxxxx.top/api/WeChatPayTest/RefundsCallback");

            return payModel;

        }

查询退款

调用方法


  	/// <summary>

        /// 查询退款结果接口

        /// </summary>

        /// <param name="refundNumber">商户系统内部的退款单号,商户系统内部唯一</param>

        /// <returns></returns>

        [HttpGet, Route("api/WeChatPayTest/QueryRefunds")]

        public async Task<QueryRefundsOrderRespModel> QueryRefunds(string refundNumber)

        {

            var helper = new WxPayHelper(WxPayConst.appid, WxPayConst.mchid, WxPayConst.serialNo, WxPayConst.privateKey);

            var payModel = await helper.QueryRefundsOrder(refundNumber);

            return payModel;

        }

退款结果通知

实现方法方法


        /// <summary>

        /// 退款通知回调接口

        /// </summary>

        /// <returns>退款通知http应答码为200且返回状态码为SUCCESS才会当做商户接收成功,否则会重试。注意:重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。</returns>

        [HttpPost, Route("api/WeChatPayTest/RefundsCallback")]

        [AllowAnonymous]

        public async Task<RefundsCallbackRespModel> RefundsCallback()

        {

            #region 获取字符串流

            /**

             * .NET 获取字符串流

             *  System.IO.Stream s = HttpContext.Current.Request.InputStream;

                int count = 0;

                byte[] buffer = new byte[1024];

                StringBuilder builder = new StringBuilder();

                while ((count = s.Read(buffer, 0, 1024)) > 0)

                {

                    builder.Append(Encoding.UTF8.GetString(buffer, 0, count));

                }

                s.Flush();

                s.Close();

                s.Dispose();

             */

            var buffer = new MemoryStream();

            Request.Body.CopyTo(buffer);

            #endregion

            //我没有使用官方的那种验证数据安全性的方法,我解密出来数据之后,直接拿着商户退款订单号再去查询一下订单状态,然后再更新到数据库中。我嫌麻烦……

            var str = Encoding.UTF8.GetString(buffer.GetBuffer());

            var wxPayNotifyModel = str.ToObject<RefundsCallbackModel>();

            var resource = wxPayNotifyModel?.resource ?? new RefundsCallbackResourceModel();

            var decryptStr = AesGcmHelper.AesGcmDecrypt(resource.associated_data, resource.nonce, resource.ciphertext, WxPayConst.APIV3Key);

            var decryptModel = decryptStr.ToObject<RefundsCallbackDecryptModel>();



            var viewModel = new RefundsCallbackRespModel();

            if (string.IsNullOrEmpty(decryptModel.out_trade_no))

            {

                viewModel.code = "FAIL";

                viewModel.message = "数据解密失败";

            }

            else

            {

                var resp = await QueryRefunds(decryptModel.out_refund_no);

                //然后进行数据库更新处理……等等其他操作

            }

            return viewModel;

        }

工具类方法

        WxPayHelper.cs

          封装好的专门提供给用户使用的方法,可以直接传参,不需要用户构造工具类、实体,只需要把调用接口所需要的参数准备好传递过来就行了。


    public class WxPayHelper

    {

        /// <summary>

        /// 直连商户申请的公众号或移动应用appid。

        /// </summary>

        private string _appid { set; get; }



        /// <summary>

        /// 直连商户的商户号,由微信支付生成并下发。

        /// </summary>

        private string _mchid { set; get; }



        /// <summary>

        /// 证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao

        /// </summary>

        private string _serialNo;



        /// <summary>

        /// 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----     亦不包括结尾的-----END PRIVATE KEY-----

        /// </summary>

        private string _privateKey;



        /// <summary>

        /// 构造函数

        /// </summary>

        /// <param name="appid">直连商户申请的公众号或移动应用appid</param>

        /// <param name="mchid">直连商户的商户号,由微信支付生成并下发</param>

        /// <param name="serialNo">证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao</param>

        /// <param name="privateKey">私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----     亦不包括结尾的-----END PRIVATE KEY-----</param>

        public WxPayHelper(string appid, string mchid, string serialNo, string privateKey)

        {

            _appid = appid;

            _mchid = mchid;

            _serialNo = serialNo;

            _privateKey = privateKey;

        }



        /// <summary>

        /// 统一下单接口,只对接了必填的字段

        /// </summary>

        /// <param name="description">商品描述</param>

        /// <param name="price">订单总金额,单位为分</param>

        /// <param name="out_trade_no">商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一</param>

        /// <param name="notify_url">通知URL必须为直接可访问的URL,不允许携带查询串。格式:URL</param>

        /// <param name="attach">附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用</param>

        /// <param name="currency">货币类型,CNY:人民币,境内商户号仅支持人民币</param>

        /// <returns></returns>

        public async Task<WxPayRespModel> UnionGenerateOrder(string description, int price, string out_trade_no, string notify_url, string attach = "", string currency = "CNY")

        {

            var url = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";

            var req = new GenerateOrderModelForWxPay

            {

                appid = this._appid,

                mchid = this._mchid,

                description = description,

                amount = new WxPayAmountModel

                {

                    total = price,

                    currency = currency

                },

                out_trade_no = out_trade_no,

                attach = attach,

                notify_url = notify_url

            };



            var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));

            var bodyJson = new StringContent(req.ToJson(), Encoding.UTF8, "application/json");



            var resp = await client.PostAsync(url, bodyJson);

            // 注意!!! 这个resp只是http的结果,需要把接口具体返回的值读取出来,如果接口报错的话,这地方可以看到具体的错误信息,我就是在这里入坑的。

            var respStr = await resp.Content.ReadAsStringAsync();



            // 如果下单成功,就解析返回的结果,把prepay_id解析出来

            var viewModel = respStr.ToObject<WxPayRespModel>();



            return viewModel;

        }



        /// <summary>

        /// 查询订单

        /// </summary>

        /// <param name="orderNumber">商户自己的订单号,不是微信生成的订单号</param>

        public async Task<WxPayStatusRespModel> QueryOrder(string orderNumber)

        {

            var url = $"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{orderNumber}?mchid={_mchid}";

            var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));

            var resp = await client.GetAsync(url);

            var respStr = await resp.Content.ReadAsStringAsync();

            var payModel = respStr.ToObject<WxPayStatusRespModel>();

            return payModel;

        }



        /// <summary>

        /// 关闭订单

        /// </summary>

        /// <param name="orderNumber">商户自己的订单号,不是微信生成的订单号</param>

        public async Task<ClostOrderRespModel> ClostOrder(string orderNumber)

        {

            var url = $"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{orderNumber}/close";

            var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));

            var bodyJson = new StringContent(new { mchid = this._mchid }.ToJson(), Encoding.UTF8, "application/json");

            var resp = await client.PostAsync(url, bodyJson);

            var respStr = await resp.Content.ReadAsStringAsync();

            var closeModel = new ClostOrderRespModel();

            if (!string.IsNullOrEmpty(respStr))

                closeModel = respStr.ToObject<ClostOrderRespModel>();

            closeModel.StatusCode = resp.StatusCode.ToString();

            return closeModel;

        }



        /// <summary>

        /// 微信支付申请退款接口

        /// </summary>

        /// <param name="out_trade_no">商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一;原支付交易对应的商户订单号。</param>

        /// <param name="out_refund_no">商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。</param>

        /// <param name="reason">若商户传入,会在下发给用户的退款消息中体现退款原因。</param>

        /// <param name="refund">退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。</param>

        /// <param name="total">原支付交易的订单总金额,币种的最小单位,只能为整数。</param>

        /// <param name="notify_url">异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。示例值:https://weixin.qq.com ,必须是https的网址</param>

        /// <param name="currency">符合ISO 4217标准的三位字母代码,目前只支持人民币:CNY。</param>

        /// <returns></returns>

        public async Task<RefundsRespModel> Refunds(string out_trade_no, string out_refund_no, string reason, int refund, int total, string notify_url = "", string currency = "CNY")

        {

            var req = new RefundsRequestModel

            {

                out_refund_no = out_refund_no,

                out_trade_no = out_trade_no,

                reason = reason,

                notify_url = notify_url,

                amount = new RefundsAmountModel

                {

                    refund = refund,

                    total = total,

                    currency = currency

                }

            };



            var url = $"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";

            var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));

            var bodyJson = new StringContent(req.ToJson(), Encoding.UTF8, "application/json");



            var resp = await client.PostAsync(url, bodyJson);

            var respStr = await resp.Content.ReadAsStringAsync();



            var payModel = respStr.ToObject<RefundsRespModel>();

            return payModel;

        }

        /// <summary>

        /// 查询退款结果

        /// </summary>

        /// <param name="out_refund_no">商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。</param>

        public async Task<QueryRefundsOrderRespModel> QueryRefundsOrder(string out_refund_no)

        {

            var url = $"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}";

            var client = new HttpClient(new WxPayRequestHandler(_mchid, _serialNo, _privateKey));

            var resp = await client.GetAsync(url);

            var respStr = await resp.Content.ReadAsStringAsync();

            var payModel = respStr.ToObject<QueryRefundsOrderRespModel>();

            return payModel;

        }

    }

Http请求方法

      WxPayRequestHandler.cs

           封装好的专用于请求微信支付接口的工具类。


    internal class WxPayRequestHandler : DelegatingHandler

    {

        /// <summary>

        /// 直连商户的商户号,由微信支付生成并下发。

        /// </summary>

        private string _merchantId = string.Empty;



        /// <summary>

        /// 证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao

        /// </summary>

        private string _serialNo = string.Empty;



        /// <summary>

        /// 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----     亦不包括结尾的-----END PRIVATE KEY-----

        /// </summary>

        private string _privateKey = string.Empty;



        /// <summary>

        /// 构造方法

        /// </summary>

        /// <param name="mchid">直连商户的商户号,由微信支付生成并下发。</param>

        /// <param name="serialNo">证书序列号,查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao</param>

        /// <param name="privateKey">私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----     亦不包括结尾的-----END PRIVATE KEY-----</param>

        public WxPayRequestHandler(string mchid, string serialNo, string privateKey)

        {

            InnerHandler = new HttpClientHandler();

            this._merchantId = mchid;

            this._serialNo = serialNo;

            this._privateKey = privateKey;

        }



        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");

            request.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");



            return await base.SendAsync(request, cancellationToken);

        }



        private async Task<string> BuildAuthAsync(HttpRequestMessage request)

        {

            var method = request.Method.ToString();

            var body = "";

            if (method == "POST" || method == "PUT" || method == "PATCH")

            {

                var c = request.Content;

                body = await c.ReadAsStringAsync();//这里读取的数据一定要跟传入的参数一致,debug时看到的数据与传入的参数不一致是不可以的

            }



            string uri = request.RequestUri.PathAndQuery;

            var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();

            string nonce = Path.GetRandomFileName();

            var message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";

            var signature = Sign(message);

            return $"mchid=\"{_merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"";

        }



        private string Sign(string message)

        {

            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));

            }

        }

    }

说在最后,代码已经上传到码云和github了,大家可以提提意见。

最后一次编辑于  2021-06-04  
点赞 4
收藏
评论

1 个评论

  • 未来
    未来
    2022-10-18

    大神,Core版本发布到Centos服务器上,小程序调起支付API,签名Sign这里报错,请问这个怎么解决,windows的签名方式linux不支持

    2022-10-18
    赞同
    回复
登录 后发表内容