C#下支付宝新版异步回调数据处理及校验(需支付宝提供的AopSdk)

版权声明:本文为starfd原创文章,转载请标明出处。 https://blog.csdn.net/starfd/article/details/79085916

对于支付宝,我们首先得赞扬下,提供了NET下的SDK,这个AopSdk当真是大而全,但同时却又不得不吐槽下,都提供了那么多的Request/Response(笔者下载的是alipay-sdk-NET20170615110549,里面光Request就有556个),但回调这一块居然一点都没提供(我确信肯定没提供,因为我按各种关键字进行了全文搜索),开放平台上也只说提供了服务端SDK,包含签名之类的功能,所以所有的接入者不得不自己写回调这一块

支付宝的回调与微信相比,除了签名外,还增加了notify_id用于校验该次请求是否是支付宝发起(注意这个id在你返回success后就失效了),所以更多了一层安全性

好了,啰嗦了这么多,还是直接来代码吧

    /// <summary>
    /// 支付宝回调辅助类
    /// </summary>
    public class AlipayNotifyHelper
    {
        /// <summary>
        /// 根据请求数据获取对应的回调实体
        /// </summary>
        /// <typeparam name="T">回调实体</typeparam>
        /// <param name="collection">请求数据</param>
        /// <returns></returns>
        public static T GetNotify<T>(NameValueCollection collection)
            where T : new()
        {
            T entity = new T();
            var props = typeof(T).GetProperties();
            foreach (var key in collection.AllKeys)
            {
                var pName = Regex.Replace(key, @"(?:^|_)([a-zA-Z])", m => m.Groups[1].Value.ToUpper());
                var p = props.FirstOrDefault(t => t.Name == pName);
                if (p != null)
                {
                    Type pType = null;
                    if (!p.PropertyType.IsGenericType)
                    {
                        pType = p.PropertyType;
                    }
                    else if (p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        pType = Nullable.GetUnderlyingType(p.PropertyType);
                    }
                    if (pType != null)
                    {
                        p.SetValue(entity, ConvertTo(collection[key], pType));
                    }
                }
            }
            return entity;
        }
        private static object ConvertTo(string value, Type type)
        {
            if (type.IsEnum)
            {
                return Enum.Parse(type, value);
            }
            else
            {
                return Convert.ChangeType(value, type);
            }
        }
        /// <summary>
        /// 对数据进行正确性校验,校验通过的话返回对应回调实体
        /// </summary>
        /// <typeparam name="T">回调实体</typeparam>
        /// <param name="collection">请求数据</param>
        /// <param name="alipayPublicKey">支付宝公钥</param>
        /// <param name="keyFromFile">是否从文件读取 true代表从文件读取</param>
        /// <param name="verifyFromAlipay">是否校验该次请求是否为支付宝发出</param>
        /// <param name="mapiUrl">校验地址</param>
        /// <param name="partnerKey">请求数据中合作伙伴号对应的键值</param>
        /// <param name="notifyIdKey">请求数据中回调id对应的键值</param>
        /// <param name="charsetKey">请求数据中charset对应的键值</param>
        /// <param name="signTypeKey">请求数据中signtype对应的键值</param>
        /// <returns></returns>
        public static async Task<T> VerifyAndGetNotify<T>(NameValueCollection collection, string alipayPublicKey, bool keyFromFile,
            bool verifyFromAlipay = true, string mapiUrl = "https://mapi.alipay.com/gateway.do", string partnerKey = "seller_id", string notifyIdKey = "notify_id", string charsetKey = "charset", string signTypeKey = "sign_type")
            where T : new()
        {
            var parameters = collection.Cast<string>().ToDictionary(k => k, v => collection[v]);
            if (Aop.Api.Util.AlipaySignature.RSACheckV1(parameters, alipayPublicKey, parameters[charsetKey], parameters[signTypeKey], keyFromFile)
                && await IsNotifiedFromAlipay(verifyFromAlipay, parameters[partnerKey], parameters[notifyIdKey], mapiUrl))
            {
                return GetNotify<T>(collection);
            }
            return default(T);
        }
        private static async Task<bool> IsNotifiedFromAlipay(bool verifyFromAlipay, string partner, string notifyId, string mapiUrl)
        {
            if (!verifyFromAlipay) return true;
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format("{0}?service=notify_verify&partner={1}&notify_id={2}", mapiUrl, partner, notifyId));
            var response = await request.GetResponseAsync();
            using (var stream = response.GetResponseStream())
            {
                using (StreamReader sr = new StreamReader(stream))
                {
                    return sr.ReadToEnd().Equals("true", StringComparison.OrdinalIgnoreCase);
                }
            }
        }
        /// <summary>
        /// 获取实体对应的签名用字典
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <returns></returns>
        public static IDictionary<string, string> GetSignDictionary<T>(T entity)
        {
            Dictionary<string, string> dic = new Dictionary<string, string>();
            var props = typeof(T).GetProperties();
            foreach (var p in props)
            {
                var pValue = p.GetValue(entity);
                if (pValue != null)
                {
                    var pKey = Regex.Replace(p.Name, @"[A-Z]", m => string.Format("{0}{1}", m.Index == 0 ? "" : "_", m.Value.ToLower()));
                    dic.Add(pKey, pValue.ToString());
                }
            }
            return dic;
        }
    }
这个类使用前提你定义的回调实体属性都是以Pascal 命名法命名,而且对于支付宝回传的复杂实体(Json)不能进行反序列化处理(但对于金额部分可以定义为decimal),以wap支付为例,以下是定义的回调实体
   /// <summary>
    /// 支付宝wap支付回调 https://docs.open.alipay.com/203/105286/
    /// </summary>
    public class AlipayTradeWapPayNotify
    {
        /// <summary>
        /// notify_time 通知时间 格式为yyyy-MM-dd HH:mm:ss
        /// </summary>
        public string NotifyTime { get; set; }
        /// <summary>
        /// notify_type 通知类型
        /// </summary>
        public string NotifyType { get; set; }
        /// <summary>
        /// notify_id 通知校验ID
        /// </summary>
        public string NotifyId { get; set; }
        /// <summary>
        /// app_id 支付宝分配给开发者的应用Id
        /// </summary>
        public string AppId { get; set; }
        /// <summary>
        /// charset 编码格式,如utf-8、gbk、gb2312等
        /// </summary>
        public string Charset { get; set; }
        /// <summary>
        /// version 调用的接口版本,固定为:1.0
        /// </summary>
        public string Version { get; set; }
        /// <summary>
        /// sign_type 签名类型,商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2
        /// </summary>
        public AlipaySignType SignType { get; set; }
        /// <summary>
        /// sign 签名
        /// </summary>
        public string Sign { get; set; }
        /// <summary>
        /// trade_no 支付宝交易凭证号
        /// </summary>
        public string TradeNo { get; set; }
        /// <summary>
        /// out_trade_no 原支付请求的商户订单号
        /// </summary>
        public string OutTradeNo { get; set; }
        /// <summary>
        /// out_biz_no 商户业务ID,主要是退款通知中返回退款申请的流水号
        /// </summary>
        public string OutBizNo { get; set; }
        /// <summary>
        /// buyer_id 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字
        /// </summary>
        public string BuyerId { get; set; }
        /// <summary>
        /// buyer_logon_id 买家支付宝账号
        /// </summary>
        public string BuyerLogonId { get; set; }
        /// <summary>
        /// seller_id 卖家支付宝用户号
        /// </summary>
        public string SellerId { get; set; }
        /// <summary>
        /// seller_email 卖家支付宝账号
        /// </summary>
        public string SellerEmail { get; set; }
        /// <summary>
        /// trade_status 交易目前所处的状态
        /// </summary>
        public AlipayTradeStatus? TradeStatus { get; set; }
        /// <summary>
        /// total_amount 本次交易支付的订单金额,单位为人民币(元)
        /// </summary>
        public decimal? TotalAmount { get; set; }
        /// <summary>
        /// receipt_amount 商家在交易中实际收到的款项,单位为元
        /// </summary>
        public decimal? ReceiptAmount { get; set; }
        /// <summary>
        /// invoice_amount 用户在交易中支付的可开发票的金额
        /// </summary>
        public decimal? InvoiceAmount { get; set; }
        /// <summary>
        /// buyer_pay_amount 用户在交易中支付的金额
        /// </summary>
        public decimal? BuyerPayAmount { get; set; }
        /// <summary>
        /// point_amount 使用集分宝支付的金额
        /// </summary>
        public decimal? PointAmount { get; set; }
        /// <summary>
        /// refund_fee 退款通知中,返回总退款金额,单位为元,支持两位小数
        /// </summary>
        public decimal? RefundFee { get; set; }
        /// <summary>
        /// subject 商品的标题/交易标题/订单标题/订单关键字等,是请求时对应的参数,原样通知回来
        /// </summary>
        public string Subject { get; set; }
        /// <summary>
        /// body 该订单的备注、描述、明细等。对应请求时的body参数,原样通知回来
        /// </summary>
        public string Body { get; set; }
        /// <summary>
        /// gmt_create 该笔交易创建的时间。格式为yyyy-MM-dd HH:mm:ss
        /// </summary>
        public string GmtCreate { get; set; }
        /// <summary>
        /// gmt_payment 该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:ss
        /// </summary>
        public string GmtPayment { get; set; }
        /// <summary>
        /// gmt_refund 该笔交易的退款时间。格式为yyyy-MM-dd HH:mm:ss.S
        /// </summary>
        public string GmtRefund { get; set; }
        /// <summary>
        /// gmt_close 该笔交易结束时间。格式为yyyy-MM-dd HH:mm:ss
        /// </summary>
        public string GmtClose { get; set; }
        /// <summary>
        /// fund_bill_list 支付成功的各个渠道金额信息
        /// 例:[{"amount":"15.00","fundChannel":"ALIPAYACCOUNT"}]
        /// </summary>
        public string FundBillList { get; set; }
        /// <summary>
        /// passback_params 公共回传参数,如果请求时传递了该参数,则返回给商户时会在异步通知时将该参数原样返回。本参数必须进行UrlEncode之后才可以发送给支付宝
        /// </summary>
        public string PassbackParams { get; set; }
        /// <summary>
        /// voucher_detail_list 本交易支付时所使用的所有优惠券信息
        /// 例:[{"amount":"0.20","merchantContribute":"0.00","name":"一键创建券模板的券名称","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","memo":"学生卡8折优惠"]
        /// </summary>
        public string VoucherDetailList { get; set; }
    }
注意凡是支付宝开放平台上注明不是必返回的值,这里都设置为了可空类型,然后是对应的几个枚举
    /// <summary>
    /// alipay签名方式
    /// </summary>
    public enum AlipaySignType
    {
        RSA2 = 0,
        RSA = 1
    }
    /// <summary>
    /// 订单支付状态
    /// </summary>
    public enum AlipayTradeStatus
    {
        /// <summary>
        /// 交易创建,等待买家付款
        /// </summary>
        WAIT_BUYER_PAY = 0,
        /// <summary>
        /// 商户签约的产品支持退款功能的前提下,买家付款成功
        /// </summary>
        TRADE_SUCCESS = 1,
        /// <summary>
        /// 交易结束,不可退款
        /// </summary>
        TRADE_FINISHED = 2,
        /// <summary>
        /// 未付款交易超时关闭,或支付完成后全额退款
        /// </summary>
        TRADE_CLOSED = 3,
    }

上面只是列举了代码,现在说下该怎么使用,因为支付宝的回调都是以application/x-www-form-urlencoded进行回调的,所以这里在方法参数上采用了NameValueCollection,即直接使用POST过来的数据,在校验通过后才返回对应的回调实体,下面分别列举Webform、MVC、WebAPI如何获取这个NameValueCollection

WebForm,这个无需举例,只需在Page_Load中,直接将Request.Form作为参数传递给VerifyAndGetNotify方法

MVC,这个可以同WebForm一样直接使用Request.Form,也可以按以下的方法定义Action,然后直接将FormCollection collection传递给VerifyAndGetNotify方法

        public ActionResult Form(FormCollection collection)
        {
	//Request.Form 也可以通过这样的方式获取
            return Redirect("Index");
        }
WebAPI,这个与上述两者皆不相同,当然这里也不考虑直接获取WebForm下的Request的方式,所以这里将以FormDataCollection来间接的获取,然后将获取到的col传递给VerifyAndGetNotify方法
        public void Post(FormDataCollection collection)
        {
            var col = collection.ReadAsNameValueCollection();
        }
说完了如何获取NameValueCollection,下面最后再列下使用的例子
string appId = "你的appid";
//这里除必输项外,所有可选参数都采用的默认值,注意默认是会向支付宝请求校验该次请求是否为支付宝发出的请求
var wapNotify = await AlipayNotifyHelper.VerifyAndGetNotify<AlipayTradeWapPayNotify>(collection, AlipayInfo.AlipayPublicKey, false);
if (wapNotify != null)
{
    if ((wapNotify.TradeStatus == AlipayTradeStatus.TRADE_SUCCESS
        || wapNotify.TradeStatus == AlipayTradeStatus.TRADE_FINISHED)
        && wapNotify.AppId == appId)//校验appid是否一致
    {
        //校验wapNotify.OutTradeNo是否正确
        //校验wapNotify.TotalAmount.Value是否一致
        //校验wapNotify.SellerId是否正确(其实就是合作伙伴号,这一步实际我没有进行校验)
        //以上校验都通过后,根据wapNotify.TradeStatus来进行相应的业务处理
    }
}
千万注意该辅助类依赖于支付宝提供的sdk,该sdk的下载地址为: https://docs.open.alipay.com/54/103419/

完整的代码及示例可在 此处下载

2018-03-09补充:git下载已不再依赖AopSdk,同时支持Standard2.0


猜你喜欢

转载自blog.csdn.net/starfd/article/details/79085916