业务背景
关于微信自动退款串接背景基于酷客多多商户系统,系统组成主要有前端小程序、商家后台管理系统、运营商系统等
业务流程
退款单状态:待退款、退款中、退款完成、自动退款失败等
由于微信申请退款接口接受请求后不会立即进行退款处理,微信此处有延迟,因此在实际业务串接中,不能依据申请退款接口调用是否成功来修改业务系统中退款单的状态;必须以微信退款通知的状态或者自行调用查看退款状态接口的状态为准
微信退款串接流程图,见下图
微信后台配置
1.申请退款接口调用需要微信证书;因此需要先到微信商户平台——账户中心——API安全中下载证书;
此证书是用于调用申请退款时使用;需要先安装到系统中
2.到微信商户平台开启退款通知配置;并配置退款通知接口地址;此地址用于接受退款结果通知的
微信申请退款接口串接常见问题
1.参数错误问题,接口要求商户订单号、退款单号、退款金额、订单金额为业务要求必传字段;
a)其中商户订单号为你要退的订单支付时传入的订单号
b)退款单号为当前微信商户号下唯一编号,按照微信官方文档生成规则即可
c)指当前订单多次退款金额合计不得超过订单金额
2.申请退款接口调用前;安装微信API证书,调用时需带着证书请求
3.接受不到微信退款通知信息问题
a)检查是否在微信商户平台配置开启退款通知,并配置通知路径
b)检查接受通知接口是否正式访问
c)通知接口必须是部署在服务器,且外网正常访问得
4.接受微信退款结果通知接口;无法解密参数问题
微信公布解密步骤如下:
(1)对加密串A做base64解码,得到加密串B
(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
实际串接时注意
a) 微信通知信息数据格式采用xml;从xml节点中取出加密数据时 ,第一步用base64解码步骤省略,因为从xml节点取出得数据已是base64解码过的字符串了
b)商户key做md5加密;此处注意商户key指得再微信商户平台配置得32商户密钥;md5加密也一定注意是32位小写;
关键代码块
微信官方未提供得代码; 解密微信退款通知信息方法
/// <summary>
/// 解密微信支付退款结果通知
/// </summary>
/// <param name="s">要解密字符串</param>
/// <param name="shkey">商户key</param>
/// <returns></returns>
public static string DecodeReqInfo(string s, string shkey)
{
string result = null;
string key = System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(shkey, "md5").ToLower(); //32位小写md5加密
result = DecodeAES256ECB(s, key);
return result ;
}
/// <summary>
/// AES-256-ECB字符解密
/// </summary>
/// <param name="s">要解密字符串</param>
/// <param name="key">密钥</param>
/// <returns></returns>
public static string DecodeAES256ECB(string s, string key)
{
string result = null;
try
{
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(key);
byte[] toEncryptArray = Convert.FromBase64String(s);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
result = UTF8Encoding.UTF8.GetString(resultArray);
}
catch { }
return result ;
}
微信退款接口串接代码段, 此代码是按照微信官方提供demo修改使用;
/// <summary>
/// 微信退款接口
/// </summary>
/// <param name="transaction_id"></param>
/// <param name="out_trade_no">订单号</param>
/// <param name="out_refund_no">退款单号</param>
/// <param name="total_fee">总金额</param>
/// <param name="refund_fee">退款金额</param>
/// <param name="wxPayConfig">微信配置参数类</param>
/// <returns></returns>
public static string Run(string transaction_id, string out_trade_no, string out_refund_no, string total_fee, string refund_fee, WxPayConfig wxPayConfig)
{
Log.Info("Refund", "Refund is processing...");
WxPayData data = new WxPayData();
if (!string.IsNullOrEmpty(transaction_id))//微信订单号存在的条件下,则已微信订单号为准
{
data.SetValue("transaction_id", transaction_id);
}
else//微信订单号不存在,才根据商户订单号去退款
{
data.SetValue("out_trade_no", out_trade_no);
}
data.SetValue("total_fee", total_fee);//订单总金额
data.SetValue("refund_fee", refund_fee);//退款金额
data.SetValue("out_refund_no", out_refund_no);//随机生成商户退款单号
data.SetValue("op_user_id", wxPayConfig.MCHID);//操作员,默认为商户号
WxPayData result = Refund(data, wxPayConfig);//提交退款申请给API,接收返回数据
Log.Info("Refund", "Refund process complete, result : " + result.ToXml());
return result.ToPrintStr();
}
/**
*
* 申请退款
* @param WxPayData inputObj 提交给申请退款API的参数
* @param int timeOut 超时时间
* @throws WxPayException
* @return 成功时返回接口调用结果,其他抛异常
*/
public static WxPayData Refund(WxPayData inputObj,WxPayConfig wxConfig ,int timeOut = 6)
{
string url = "https://api.mch.weixin.qq.com/secapi/pay/refund";
//检测必填参数
if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))
{
throw new WxPayException("退款申请接口中,out_trade_no、transaction_id至少填一个!");
}
else if (!inputObj.IsSet("out_refund_no"))
{
throw new WxPayException("退款申请接口中,缺少必填参数out_refund_no!");
}
else if (!inputObj.IsSet("total_fee"))
{
throw new WxPayException("退款申请接口中,缺少必填参数total_fee!");
}
else if (!inputObj.IsSet("refund_fee"))
{
throw new WxPayException("退款申请接口中,缺少必填参数refund_fee!");
}
else if (!inputObj.IsSet("op_user_id"))
{
throw new WxPayException("退款申请接口中,缺少必填参数op_user_id!");
}
inputObj.SetValue("appid", wxConfig.APPID);//公众账号ID
inputObj.SetValue("mch_id", wxConfig.MCHID);//商户号
inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串
inputObj.SetValue("sign", inputObj.MakeSign(wxConfig));//签名
string xml = inputObj.ToXml();
var start = DateTime.Now;
Log.Debug("WxPayApi", "Refund request : " + xml);
string response = HttpService.Post(xml, url, true, timeOut, wxConfig);//调用HTTP通信接口提交数据到API
Log.Debug("WxPayApi", "Refund response : " + response);
var end = DateTime.Now;
int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时
//将xml格式的结果转换为对象以返回
WxPayData result = new WxPayData();
result.FromXml(response, wxConfig);
//ReportCostTime(url, timeCost, result, wxConfig);//测速上报
return result;
}
/// <summary>
/// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构,
/// 在调用接口之前先填充各个字段的值,然后进行接口通信,
/// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构,
/// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构
/// </summary>
public class WxPayData
{
public WxPayData()
{
}
//采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序
private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();
/**
* 设置某个字段的值
* @param key 字段名
* @param value 字段值
*/
public void SetValue(string key, object value)
{
m_values[key] = value;
}
/**
* 根据字段名获取某个字段的值
* @param key 字段名
* @return key对应的字段值
*/
public object GetValue(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
return o;
}
/**
* 判断某个字段是否已设置
* @param key 字段名
* @return 若字段key已被设置,则返回true,否则返回false
*/
public bool IsSet(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
if (null != o)
return true;
else
return false;
}
/**
* @将Dictionary转成xml
* @return 经转换得到的xml串
* @throws WxPayException
**/
public string ToXml()
{
//数据为空时不能转化为xml格式
if (0 == m_values.Count)
{
Log.Error(this.GetType().ToString(), "WxPayData数据为空!");
throw new WxPayException("WxPayData数据为空!");
}
string xml = "<xml>";
foreach (KeyValuePair<string, object> pair in m_values)
{
//字段值不能为null,会影响后续流程
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
if (pair.Value.GetType() == typeof(int))
{
xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">";
}
else if (pair.Value.GetType() == typeof(string))
{
xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
}
else//除了string和int类型不能含有其他数据类型
{
Log.Error(this.GetType().ToString(), "WxPayData字段数据类型错误!");
throw new WxPayException("WxPayData字段数据类型错误!");
}
}
xml += "</xml>";
return xml;
}
/**
* @将xml转为WxPayData对象并返回对象内部的数据
* @param string 待转换的xml串
* @return 经转换得到的Dictionary
* @throws WxPayException
*/
public SortedDictionary<string, object> FromXml(string xml, WxPayConfig wxConfig)
{
if (string.IsNullOrEmpty(xml))
{
Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
throw new WxPayException("将空的xml串转换为WxPayData不合法!");
}
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
XmlNodeList nodes = xmlNode.ChildNodes;
foreach (XmlNode xn in nodes)
{
XmlElement xe = (XmlElement)xn;
m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
}
try
{
//2015-06-29 错误是没有签名
if(m_values["return_code"] != "SUCCESS")
{
return m_values;
}
CheckSign(wxConfig);//验证签名,不通过会抛异常
}
catch(WxPayException ex)
{
throw new WxPayException(ex.Message);
}
return m_values;
}
/**
* @Dictionary格式转化成url参数格式
* @ return url格式串, 该串不包含sign字段值
*/
public string ToUrl()
{
string buff = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
/**
* @Dictionary格式化成Json
* @return json串数据
*/
public string ToJson()
{
string jsonStr = JsonMapper.ToJson(m_values);
return jsonStr;
}
/**
* @values格式化成能在Web页面上显示的结果(因为web页面上不能直接输出xml格式的字符串)
*/
public string ToPrintStr()
{
string str = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
str += string.Format("{0}={1}<br>", pair.Key, pair.Value.ToString());
}
Log.Debug(this.GetType().ToString(), "Print in Web Page : " + str);
return str;
}
/**
* @生成签名,详见签名生成算法
* @return 签名, sign字段不参加签名
*/
public string MakeSign(WxPayConfig wxConfig)
{
//转url格式
string str = ToUrl();
//在string后加入API KEY
str += "&key=" + wxConfig.KEY;
//MD5加密
var md5 = MD5.Create();
var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var sb = new StringBuilder();
foreach (byte b in bs)
{
sb.Append(b.ToString("x2"));
}
//所有字符转为大写
return sb.ToString().ToUpper();
}
/**
*
* 检测签名是否正确
* 正确返回true,错误抛异常
*/
public bool CheckSign(WxPayConfig wxConfig)
{
//如果没有设置签名,则跳过检测
if (!IsSet("sign"))
{
Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
throw new WxPayException("WxPayData签名存在但不合法!");
}
//如果设置了签名但是签名为空,则抛异常
else if(GetValue("sign") == null || GetValue("sign").ToString() == "")
{
Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
throw new WxPayException("WxPayData签名存在但不合法!");
}
//获取接收到的签名
string return_sign = GetValue("sign").ToString();
//在本地计算新的签名
string cal_sign = MakeSign(wxConfig);
if (cal_sign == return_sign)
{
return true;
}
Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!");
throw new WxPayException("WxPayData签名验证错误!");
}
/**
* @获取Dictionary
*/
public SortedDictionary<string, object> GetValues()
{
return m_values;
}
}
/// <summary>
/// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构,
/// 在调用接口之前先填充各个字段的值,然后进行接口通信,
/// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构,
/// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构
/// </summary>
public class WxPayData
{
public WxPayData()
{
}
//采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序
private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();
/**
* 设置某个字段的值
* @param key 字段名
* @param value 字段值
*/
public void SetValue(string key, object value)
{
m_values[key] = value;
}
/**
* 根据字段名获取某个字段的值
* @param key 字段名
* @return key对应的字段值
*/
public object GetValue(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
return o;
}
/**
* 判断某个字段是否已设置
* @param key 字段名
* @return 若字段key已被设置,则返回true,否则返回false
*/
public bool IsSet(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
if (null != o)
return true;
else
return false;
}
/**
* @将Dictionary转成xml
* @return 经转换得到的xml串
* @throws WxPayException
**/
public string ToXml()
{
//数据为空时不能转化为xml格式
if (0 == m_values.Count)
{
Log.Error(this.GetType().ToString(), "WxPayData数据为空!");
throw new WxPayException("WxPayData数据为空!");
}
string xml = "<xml>";
foreach (KeyValuePair<string, object> pair in m_values)
{
//字段值不能为null,会影响后续流程
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
if (pair.Value.GetType() == typeof(int))
{
xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">";
}
else if (pair.Value.GetType() == typeof(string))
{
xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
}
else//除了string和int类型不能含有其他数据类型
{
Log.Error(this.GetType().ToString(), "WxPayData字段数据类型错误!");
throw new WxPayException("WxPayData字段数据类型错误!");
}
}
xml += "</xml>";
return xml;
}
/**
* @将xml转为WxPayData对象并返回对象内部的数据
* @param string 待转换的xml串
* @return 经转换得到的Dictionary
* @throws WxPayException
*/
public SortedDictionary<string, object> FromXml(string xml, WxPayConfig wxConfig)
{
if (string.IsNullOrEmpty(xml))
{
Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
throw new WxPayException("将空的xml串转换为WxPayData不合法!");
}
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
XmlNodeList nodes = xmlNode.ChildNodes;
foreach (XmlNode xn in nodes)
{
XmlElement xe = (XmlElement)xn;
m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
}
try
{
//2015-06-29 错误是没有签名
if(m_values["return_code"] != "SUCCESS")
{
return m_values;
}
CheckSign(wxConfig);//验证签名,不通过会抛异常
}
catch(WxPayException ex)
{
throw new WxPayException(ex.Message);
}
return m_values;
}
/**
* @Dictionary格式转化成url参数格式
* @ return url格式串, 该串不包含sign字段值
*/
public string ToUrl()
{
string buff = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
/**
* @Dictionary格式化成Json
* @return json串数据
*/
public string ToJson()
{
string jsonStr = JsonMapper.ToJson(m_values);
return jsonStr;
}
/**
* @values格式化成能在Web页面上显示的结果(因为web页面上不能直接输出xml格式的字符串)
*/
public string ToPrintStr()
{
string str = "";
foreach (KeyValuePair<string, object> pair in m_values)
{
if (pair.Value == null)
{
Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
throw new WxPayException("WxPayData内部含有值为null的字段!");
}
str += string.Format("{0}={1}<br>", pair.Key, pair.Value.ToString());
}
Log.Debug(this.GetType().ToString(), "Print in Web Page : " + str);
return str;
}
/**
* @生成签名,详见签名生成算法
* @return 签名, sign字段不参加签名
*/
public string MakeSign(WxPayConfig wxConfig)
{
//转url格式
string str = ToUrl();
//在string后加入API KEY
str += "&key=" + wxConfig.KEY;
//MD5加密
var md5 = MD5.Create();
var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var sb = new StringBuilder();
foreach (byte b in bs)
{
sb.Append(b.ToString("x2"));
}
//所有字符转为大写
return sb.ToString().ToUpper();
}
/**
*
* 检测签名是否正确
* 正确返回true,错误抛异常
*/
public bool CheckSign(WxPayConfig wxConfig)
{
//如果没有设置签名,则跳过检测
if (!IsSet("sign"))
{
Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
throw new WxPayException("WxPayData签名存在但不合法!");
}
//如果设置了签名但是签名为空,则抛异常
else if(GetValue("sign") == null || GetValue("sign").ToString() == "")
{
Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
throw new WxPayException("WxPayData签名存在但不合法!");
}
//获取接收到的签名
string return_sign = GetValue("sign").ToString();
//在本地计算新的签名
string cal_sign = MakeSign(wxConfig);
if (cal_sign == return_sign)
{
return true;
}
Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!");
throw new WxPayException("WxPayData签名验证错误!");
}
/**
* @获取Dictionary
*/
public SortedDictionary<string, object> GetValues()
{
return m_values;
}
}
/***
* 退款查询完整业务流程逻辑
* @param refund_id 微信退款单号(优先使用)
* @param out_refund_no 商户退款单号
* @param transaction_id 微信订单号
* @param out_trade_no 商户订单号
* @return 退款查询结果(xml格式)
*/
public static string Run(string refund_id, string out_refund_no, string transaction_id, string out_trade_no, WxPayConfig wxConfig)
{
Log.Info("RefundQuery", "RefundQuery is processing...");
WxPayData data = new WxPayData();
if(!string.IsNullOrEmpty(refund_id))
{
data.SetValue("refund_id", refund_id);//微信退款单号,优先级最高
}
else if(!string.IsNullOrEmpty(out_refund_no))
{
data.SetValue("out_refund_no", out_refund_no);//商户退款单号,优先级第二
}
else if(!string.IsNullOrEmpty(transaction_id))
{
data.SetValue("transaction_id", transaction_id);//微信订单号,优先级第三
}
else
{
data.SetValue("out_trade_no", out_trade_no);//商户订单号,优先级最低
}
WxPayData result = RefundQuery(data, wxConfig);//提交退款查询给API,接收返回数据
Log.Info("RefundQuery", "RefundQuery process complete, result : " + result.ToXml());
return result.ToPrintStr();
}
/**
*
* 查询退款
* 提交退款申请后,通过该接口查询退款状态。退款有一定延时,
* 用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态。
* out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个
* @param WxPayData inputObj 提交给查询退款API的参数
* @param int timeOut 接口超时时间
* @throws WxPayException
* @return 成功时返回,其他抛异常
*/
public static WxPayData RefundQuery(WxPayData inputObj,WxPayConfig wxConfig, int timeOut = 6)
{
string url = "https://api.mch.weixin.qq.com/pay/refundquery";
//检测必填参数
if(!inputObj.IsSet("out_refund_no") && !inputObj.IsSet("out_trade_no") &&
!inputObj.IsSet("transaction_id") && !inputObj.IsSet("refund_id"))
{
throw new WxPayException("退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个!");
}
inputObj.SetValue("appid", wxConfig.APPID);//公众账号ID
inputObj.SetValue("mch_id", wxConfig.MCHID);//商户号
inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串
inputObj.SetValue("sign", inputObj.MakeSign(wxConfig));//签名
string xml = inputObj.ToXml();
var start = DateTime.Now;//请求开始时间
Log.Debug("WxPayApi", "RefundQuery request : " + xml);
string response = HttpService.Post(xml, url, false, timeOut, wxConfig);//调用HTTP通信接口以提交数据到API
Log.Debug("WxPayApi", "RefundQuery response : " + response);
var end = DateTime.Now;
int timeCost = (int)((end-start).TotalMilliseconds);//获得接口耗时
//将xml格式的结果转换为对象以返回
WxPayData result = new WxPayData();
result.FromXml(response, wxConfig);
ReportCostTime(url, timeCost, result, wxConfig);//测速上报
return result;
}