关于微信支付,支付宝支付与银联支付的异步消息状态
最近发现支付系统异步消息老是会重发,微信支付的订单每天都是重复推送消息,支付宝支付时间相隔一年多居然还会推送支付消息
微信支付
最近發現微信支付的订单每天都是重复推送消息,官方的文档中也说明“商户系统必须能够正确处理重复的通知”
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。
微信交易只需要验证通信标识与交易标识很简单
//验证参数签名 string signK = _weixinService.GenerateWxSign(WxPayConfig.KEY, dict); if (signK != dict["sign"]) { logger.Info("WeixinController Wap Notify Verify WxSign Error : 请求报文=[signK " + signK + " : dict['sign'] " + dict["sign"] + "]\n"); return Content(AjaxResult.Error("WeixinController Wap Notify Verify WxSign Error " + "]\n").ToStringSafe()); } //验证通信标识 string return_code = dict["return_code"]; if (!return_code.Equals("SUCCESS", StringComparison.OrdinalIgnoreCase)) { string return_msg = dict["return_msg"]; logger.Info("WeixinController Wap Notify return_code Error : 请求报文=[" + return_code + " : " + return_msg + "]\n"); return Content(AjaxResult.Error("WeixinController Wap Notify return_code Error " + return_code + "]\n").ToStringSafe()); } //验证交易标识[重要!!!] string result_code = dict["result_code"]; if (!result_code.Equals("SUCCESS", StringComparison.OrdinalIgnoreCase)) { string err_code = dict["err_code"]; string err_code_des = dict["err_code_des"]; logger.Info("WeixinController Wap Notify return_code Error : 请求报文=[" + err_code + " : " + err_code_des + "]\n"); return Content(AjaxResult.Error("WeixinController Wap Notify return_code Error " + result_code + "]\n").ToStringSafe()); }
在之前的V3微信支付中,处理业务完成后,返回消息
//构造返回成功XML var wxdict = new SortedDictionary<string, string>(); wxdict.Add("return_code", "SUCCESS"); wxdict.Add("return_msg", "PAY_SUCCESS"); string wxRXml = wxdict.ConvertWxDictToString(); logger.Info("WeixinController Wap Notify Success wxRXml : " + wxRXml + " 】"); return Content(wxRXml);
ConvertWxDictToString 是一个扩展方法
/// <summary> /// 字典转微信返回XML /// </summary> /// <typeparam name="TKey"></typeparam> /// <typeparam name="TValue"></typeparam> /// <param name="wxdict"></param> /// <returns></returns> public static string ConvertWxDictToString<TKey, TValue>(this IDictionary<TKey, TValue> wxdict) { try { string xml = "<xml>"; wxdict.ForEach(wd => { if (wd.Value.GetType() == typeof(int)) { xml += "<" + wd.Key + ">" + wd.Value + "</" + wd.Key + ">"; } else if (wd.Value.GetType() == typeof(string)) { xml += "<" + wd.Key + ">" + "<![CDATA[" + wd.Value + "]]></" + wd.Key + ">"; } }); xml += "</xml>"; return xml; } catch (Exception) { return string.Empty; } }
后面发现消息会重复推送,后面看官网的文档是返回
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> </xml>
但是官方SDK还有一些其他的参数
//统一下单成功,则返回成功结果给微信支付后台 WxPayData data = new WxPayData(); data.SetValue("return_code", "SUCCESS"); data.SetValue("return_msg", "OK"); data.SetValue("appid", WxPayConfig.APPID); data.SetValue("mch_id", WxPayConfig.MCHID); data.SetValue("nonce_str", WxPayApi.GenerateNonceStr()); data.SetValue("prepay_id", unifiedOrderResult.GetValue("prepay_id")); data.SetValue("result_code", "SUCCESS"); data.SetValue("err_code_des", "OK"); data.SetValue("sign", data.MakeSign()); Log.Info(this.GetType().ToString(), "UnifiedOrder success , send data to WeChat : " + data.ToXml()); page.Response.Write(data.ToXml()); page.Response.End();
初步怀疑是contentType问题,后面我按照官方文档修改为
//构造返回成功XML var wxdict = new SortedDictionary<string, string>(); wxdict.Add("return_code", "SUCCESS"); wxdict.Add("return_msg", "OK"); string wxRXml = wxdict.ConvertWxDictToString(); logger.Info("WeixinController Wap Notify Success wxRXml : " + wxRXml + " 】"); return Content(wxRXml, @"text/xml", System.Text.Encoding.UTF8);
按照官方SDK修改
更新[未解决]:
2015年12月30日 在OSCHINA上发现也有人遇到这个问题,http://www.oschina.net/question/560841_225367?fromerr=1cloKhwT
支付宝支付
支付宝的问题是前一年的支付的订单异步消息,还会推送,对比两次推送的参数异同,第一次是支付完成,后面一次的消息是交易关闭。
--trade_status=TRADE_SUCCESS
--gmt_close=2015-12-30 03:52:12&trade_status=TRADE_FINISHED
其实支付宝消息trade_status状态有很多种
注意其中两点
1.商户必须判断商户网站中是否已经对该次的通知结果数据做过同样处理。如果不判断,存在潜在的风险,商户自行承担因此而产生的所有损失。
2.当商户使用站内退款时,系统会发送包含refund_status和gmt_refund字段的通知给商户。
官方说明,如果交易付款完成时发送的交易状态是TRADE_SUCCESS(可对交易做其他操作,如退款、分润等),则当超过签约合同指定的可退款时间段时,支付宝会主动发送 TRADE_FINISHED(不能对该交易再做任何操作)交易状态。此时,需要根据商户自身业务情况,来判断是否需对这次的交易完成通知进一步处理。即哪些条件会触发消息推送,创建订单,支付成功,交易完成,交易关闭 四种,其中在商户后台中对原始交易号手动退款也会发送异步消息推送。
官方的SDK
if (Request.Form["trade_status"] == "TRADE_FINISHED") { //判断该笔订单是否在商户网站中已经做过处理 //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序 //如果有做过处理,不执行商户的业务程序 //注意: //该种交易状态只在两种情况下出现 //1、开通了普通即时到账,买家付款成功后。 //2、开通了高级即时到账,从该笔交易成功时间算起,过了签约时的可退款时限(如:三个月以内可退款、一年以内可退款等)后。 } else if(Request.Form["trade_status"] == "TRADE_SUCCESS") { //判断该笔订单是否在商户网站中已经做过处理 //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序 //如果有做过处理,不执行商户的业务程序 //注意: //该种交易状态只在一种情况下出现——开通了高级即时到账,买家付款成功后。 } else { } //——请根据您的业务逻辑来编写程序(以上代码仅作参考)—— Response.Write("success");
官方的SDK很容易给人误导,TRADE_SUCCESS状态代表了充值成功,也就是说钱已经进了支付宝(担保交易)或卖家(即时到账)时候,这笔交易应该还可以进行后续的操作(比如一年以后交易状态自动变成TRADE_FINISHED),因为整笔交易还没有关闭掉,也就是说一定还有主动通知过来。而TRADE_FINISHED代表了这笔订单彻底完成了,不会再有任何主动通知过来了。
我的修改
/// <summary> /// 支付宝支付统一异步回调 /// </summary> public class AlipayController : Controller { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); /// <summary> /// 消息服务 /// </summary> private readonly IMessageService _messageService; /// <summary> /// 整合支付服务 /// </summary> private readonly IPayPalService _payPalService; /// <summary> /// 构造函数 /// </summary> /// <param name="payPalService">整合支付服务</param> /// <param name="messageService">消息服务</param> public AlipayController(IPayPalService payPalService, IMessageService messageService) { _payPalService = payPalService; _messageService = messageService; } #region 支付宝支付统一异步回调 /// <summary> /// 支付宝支付统一异步回调 /// </summary> /// <returns></returns> public async Task<ActionResult> Notify() { //获取参数 var param = new Dictionary<string, string>(); var item = Request.Form.AllKeys; for (int i = 0; i < item.Length; i++) { param.Add(item[i], Request.Form[item[i]]); } //param = @"discount=0.00&extra_common_param=xxxxxxx&payment_type=8&subject=会员充值&trade_no=2015040900001000790051014543&buyer_email=xxxx&gmt_create=2015-04-09 18:25:33¬ify_type=trade_status_sync&quantity=1&out_trade_no=20150401172154598&seller_id=2088701999588512¬ify_time=2015-04-09 18:26:26&body=支付宝扫码支付&trade_status=TRADE_SUCCESS&is_total_fee_adjust=N&total_fee=0.01&gmt_payment=2015-04-09 18:26:26&seller_email=xxxxxxxx&price=0.01&buyer_id=2088202878902790¬ify_id=88f196ccd6bd29b176ee805f7e48cc886e&use_coupon=N&sign_type=MD5&sign=1177becf675fa90e3182e3a97d194c53".ConvertStringToDictionary(); if (param.Count <= 0) { return Content(AjaxResult.Error("NO Notify PARAMS!").ToStringSafe()); } logger.Info("AlipayController Notify SDKUtil.CoverStringToDictionary : " + param.BuildParam()); try { string orderNo = string.Empty; #region WAP解密RSA数据 if (param.ContainsKey("notify_data") && param.ContainsKey("service") && "alipay.wap.trade.create.direct".Equals(param["service"], StringComparison.OrdinalIgnoreCase)) { var aliWapNotify = new Homeinns.SDKAPI.Pay.Wap.AlipayNotify(); //验证签名 bool verifyWapResult = aliWapNotify.VerifyNotify(param, param["sign"].ToStringSafe()); if (!verifyWapResult) { logger.Info("AlipayController WAP Notify VerifyNotify FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("VERIFY FAIL !").ToStringSafe()); } //解密(如果是RSA签名需要解密,如果是MD5签名则下面一行清注释掉) var paramDecrypt = aliWapNotify.Decrypt(param); var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(param["notify_data"]); //商户订单号 orderNo = xmlDoc.SelectSingleNode("/notify/out_trade_no").InnerText; } #endregion orderNo = param.ContainsKey("out_trade_no") ? param["out_trade_no"] : orderNo; //根据订单号判断来源 var payPal = _payPalService.Query(orderNo); if (payPal.IsNull() || payPal.NotifyUrl.IsNullOrEmpty() || payPal.OutTradeNO.IsNullOrEmpty()) { logger.Info("AlipayController Notify NOT FIND " + orderNo + " ORDER SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("NOT FIND ORDER !").ToStringSafe()); } #region 处理WEB的业务逻辑 if (payPal.Service.Equals(PayPalEnumType.ALIPAY_WEB.ToString(), StringComparison.OrdinalIgnoreCase) || payPal.Service.Equals(PayPalEnumType.ALIPAY_MMT.ToString(), StringComparison.OrdinalIgnoreCase)) { //验证交易状态 if (!param.ContainsKey("trade_status") || param["trade_status"].ContainsAny("TRADE_CLOSED", "WAIT_BUYER_PAY")) { return Content("success");
} if (!param["trade_status"].ContainsAny("TRADE_FINISHED", "TRADE_SUCCESS")) { return Content(AjaxResult.Error("Trade_status VERIFY FAIL !").ToStringSafe()); } var aliNotify = new Homeinns.SDKAPI.Pay.Alipay.AlipayNotify(); bool verifyResult = aliNotify.Verify(param.ToSortedDictionary<string, string>(), param["notify_id"], param["sign"]); //验证失败 if (!verifyResult) { logger.Info("AlipayController WEB Notify VERIFY FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("VERIFY FAIL !").ToStringSafe()); } // 异步通知的时间 string notify_time = param["notify_time"]; // 商户订单号 string out_trade_no = param["out_trade_no"]; // 支付宝交易号 string trade_no = param["trade_no"]; // 交易状态 string trade_status = param["trade_status"]; // 商品名称(会员账户号) string subject = param["subject"]; // 卖家支付宝账号 string seller_email = param["seller_email"]; // 买家支付宝账号 string buyer_email = param["buyer_email"]; // 交易金额 string total_fee = param["total_fee"]; // 交易银行 string body = param["body"]; //签名方式 string sign_type = param["sign_type"]; // 公用回传参数(传充值会员账户号) string extra_common_param = param["extra_common_param"].IsNullOrEmpty() ? string.Empty : param["extra_common_param"]; // 正式环境充值金额(将元转为分) //string amount = Convert.ToInt32(float.Parse(total_fee) * 100).ToString(); payPal.TradeNO = trade_no; payPal.TotalFee = total_fee; payPal.Service = payPal.Service + "_Notify"; payPal.CreateTime = DateTime.Parse(notify_time); payPal.Memo = param.ConvertDictionaryToJson(); //param.SerializeJson(System.Text.Encoding.UTF8); bool flag = _payPalService.Save(payPal); if (!flag) { logger.Info("AlipayController Notify SAVE PayPalData FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("SAVE PayPalData FAIL !").ToStringSafe()); } try { //调用业务接口 var data = await payPal.NotifyUrl.PostJsonAsync(payPal).ReceiveJson<WebAPIResponse>(); logger.Info("PostJsonAsync : " + payPal.SerializeJson(System.Text.Encoding.UTF8)); if (data.IsError) { logger.Fatal("PostJsonAsync : " + payPal.SerializeJson(System.Text.Encoding.UTF8)); return Content(AjaxResult.Error("PostJsonAsync FAIL !").ToStringSafe()); } } catch (Exception ex) { logger.Fatal("PostJsonAsync Exception : " + ex.Message + " payPal : " + payPal.SerializeJson(System.Text.Encoding.UTF8), ex); return Content(AjaxResult.Error("PostJsonAsync Exception !").ToStringSafe()); } return Content("success"); } #endregion #region 处理WAB的业务逻辑 if (payPal.Service.Equals(PayPalEnumType.ALIPAY_WAP.ToString(), StringComparison.OrdinalIgnoreCase)) { //XML解析notify_data数据 var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(param["notify_data"]); //商户订单号 string out_trade_no = xmlDoc.SelectSingleNode("/notify/out_trade_no").InnerText; //支付宝交易号 string trade_no = xmlDoc.SelectSingleNode("/notify/trade_no").InnerText; //交易状态 string trade_status = xmlDoc.SelectSingleNode("/notify/trade_status").InnerText; //交易金额 string total_fee = xmlDoc.SelectSingleNode("/notify/total_fee").InnerText; //商品名称 string subject = xmlDoc.SelectSingleNode("/notify/subject").InnerText; //通知时间 string notify_time = xmlDoc.SelectSingleNode("/notify/notify_time").InnerText; //验证交易状态 if (trade_status.ContainsAny("TRADE_CLOSED", "WAIT_BUYER_PAY")) { return Content("success"); } if (!trade_status.ContainsAny("TRADE_FINISHED", "TRADE_SUCCESS")) { return Content(AjaxResult.Error("Trade_status VERIFY FAIL !").ToStringSafe()); } payPal.TradeNO = trade_no; payPal.TotalFee = total_fee; payPal.Service = payPal.Service + "_Notify"; payPal.CreateTime = DateTime.Parse(notify_time); payPal.Memo = string.Empty; bool flag = _payPalService.Save(payPal); if (!flag) { logger.Info("AlipayController Notify SAVE PayPalData FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("SAVE PayPalData FAIL !").ToStringSafe()); } try { //调用业务接口 var resp = await payPal.NotifyUrl.PostJsonAsync(payPal).ReceiveJson<WebAPIResponse>(); logger.Info("PostJsonAsync payPal: " + payPal.SerializeJson(System.Text.Encoding.UTF8) + " data: " + resp.SerializeJson(System.Text.Encoding.UTF8)); if (resp.IsError) { logger.Fatal("PostJsonAsync : " + payPal.SerializeJson(System.Text.Encoding.UTF8)); return Content(AjaxResult.Error("PostJsonAsync FAIL !").ToStringSafe()); } } catch (Exception ex) { logger.Fatal("PostJsonAsync Exception : " + ex.Message + " payPalNotifyUrl: " + payPal.NotifyUrl + " payPalJson : " + payPal.SerializeJson(System.Text.Encoding.UTF8), ex); return Content(AjaxResult.Error("PostJsonAsync Exception !").ToStringSafe()); } return Content("success"); } #endregion #region 处理App的业务逻辑 if (payPal.Service.Equals(PayPalEnumType.ALIPAY_APP.ToString(), StringComparison.OrdinalIgnoreCase)) { //验证交易状态[重要!!!] if (!param.ContainsKey("trade_status") || param["trade_status"].ContainsAny("TRADE_CLOSED", "WAIT_BUYER_PAY")) { return Content("success");
} if (!param["trade_status"].ContainsAny("TRADE_FINISHED", "TRADE_SUCCESS")) { return Content(AjaxResult.Error("Trade_status VERIFY FAIL !").ToStringSafe()); } //验证签名 var aliNotify = new Homeinns.SDKAPI.Pay.App.AlipayNotify(); bool aliVerify = aliNotify.Verify(param.ToSortedDictionary<string, string>(), param["notify_id"], param["sign"]); if (!aliVerify) { logger.Info("AlipayController App Notify VERIFY FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("AlipayController App Notify VERIFY FAIL !").ToStringSafe()); } // 异步通知的时间 string notify_time = param["notify_time"]; // 商户订单号 string out_trade_no = param["out_trade_no"]; // 支付宝交易号 string trade_no = param["trade_no"]; // 交易状态 string trade_status = param["trade_status"]; // 商品名称(会员账户号) string subject = param["subject"]; // 卖家支付宝账号 string seller_email = param["seller_email"]; // 买家支付宝账号 string buyer_email = param["buyer_email"]; // 交易金额 string total_fee = param["total_fee"]; // 交易银行 string body = param["body"]; //签名方式 string sign_type = param["sign_type"]; //支付的参数 payPal.TradeNO = trade_no; payPal.TotalFee = total_fee; payPal.Service = payPal.Service + "_Notify"; payPal.CreateTime = DateTime.Parse(notify_time); payPal.Memo = param.ConvertDictionaryToJson(); bool flag = _payPalService.Save(payPal); if (!flag) { logger.Info("AlipayController App Notify SAVE PayPalData FAIL SDKUtil.CoverStringToDictionary : " + param.BuildParam()); return Content(AjaxResult.Error("SAVE PayPalData FAIL !").ToStringSafe()); } try { //调用业务接口 var data = await payPal.NotifyUrl.PostJsonAsync(payPal).ReceiveJson<WebAPIResponse>(); logger.Info("PostJsonAsync : " + payPal.SerializeJson(System.Text.Encoding.UTF8)); if (data.IsError) { logger.Fatal("PostJsonAsync : " + payPal.SerializeJson(System.Text.Encoding.UTF8)); return Content(AjaxResult.Error("PostJsonAsync FAIL !").ToStringSafe()); } } catch (Exception ex) { logger.Fatal("PostJsonAsync Exception : " + ex.Message + " payPal : " + payPal.SerializeJson(System.Text.Encoding.UTF8), ex); return Content(AjaxResult.Error("PostJsonAsync Exception !").ToStringSafe()); } return Content("success"); } #endregion } catch (Exception ex) { logger.Fatal(ex, "AlipayController App Notify Exception : " + ex.Message); } return Content(AjaxResult.Error("Notify ERROR !").ToStringSafe()); } #endregion }
综上所述,收到TRADE_FINISHED请求后,这笔订单就结束了,支付宝不会再主动请求商户网站了;收到TRADE_SUCCESS请求后,后续一定还有至少一条通知记录,即TRADE_FINISHED。所以在做通知接口时,切记使用判断订单状态用或的关系。
更新
2015年12月30日 支付宝接入文档中TRADE_SUCCESS和TRADE_FINISHED的本质区别
http://neutra.github.io/2013/%E6%94%AF%E4%BB%98%E5%AE%9DWAP%E6%94%AF%E4%BB%98%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91/
银联支付
注意事项:
1.必须能够正确处理重复的通知
2.必须做消息签名验证