【.net core】微信公众号JSAPI支付--统一下单


项目结构的一些基本说明

现在公司项目基本都是.net core框架,并且用到仓储Repository
需要在Startup中绑定服务,并注入

微信公众号支付的准备工作

前期配置,见微信支付开发文档 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3
简单说明:
商户平台中开通JSAPI支付
(1)微信商户平台-->产品中心-->开发配置-->公众号支付-->支付授权目录 ,配置好域名
(2)微信商户平台,拿到商户号商户Key,代码中需要用到
(3)微信公众平台-->公众号设置-->网页授权域名、JS安全域名,通过设置这个可以拿到code进而获取到openid及用户信息,支付这里只需要拿到openid即可

关于支付流程的一些说明

前端页面通过点击支付按钮调支付的接口
在支付接口中调用统一下单,获得下单结果
从统一下单成功返回的数据中获取微信浏览器调起jsapi支付所需的参数
前端页面拿到参数唤起微信支付
支付成功后返回ok前端可以处理其他的跳转操作

直接上代码

页面代码(简单的测试页面,页面很简陋,是非常简陋_

<!DOCTYPE html>
<html>
<head>
    <title>pay</title>
</head>

<body>
    <div class="row">
        <input type="button" value="pay" id="btnpay" />
    </div>
</body>

</html>

<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
    var _wxJsApiParam;
    function callpay() {
        if (typeof WeixinJSBridge == "undefined") {
            if (document.addEventListener) {
                document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
            }
            else if (document.attachEvent) {
                document.attachEvent('WeixinJSBridgeReady', jsApiCall);
                document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
            }
        }
        else {
            jsApiCall();
        }
    }

    //调用微信JS api 支付
    function jsApiCall() {
        WeixinJSBridge.invoke('getBrandWCPayRequest', _wxJsApiParam,
            function (res) {
                if (res.err_msg == "get_brand_wcpay_request:cancel") {
                    $.messager.alert('提示信息', '支付已经取消!', 'info');
                    return false;
                } else if (res.err_msg == "get_brand_wcpay_request:ok") {
                    //支付成功,前端可以做一些相应处理
                    alert("ok");
                }
            });
    };
	
    $(function(){
    $('#btnpay').click(function(){
         $.ajax({
             type: "GET",
             url: "xxx/Test/",
             success: function(data){
			 alert("success");
                         _wxJsApiParam = eval('(' + data + ')');
			 alert(_wxJsApiParam);
			 callpay();
                      }
         });
    });
});
</script>

Controller

[Route("test")]
[ApiController]
public class TicketController : ControllerBase
{
    ITicketRepository _ticketRepository;

    public TicketController(ITicketRepository ticketRepository)
    {
        _ticketRepository = ticketRepository;
    }

    [HttpGet("Test")]
    public IActionResult Test()
    {
        var result = _ticketRepository.Test();
        if (result.result == false)
            return BadRequest(result.msg);
        return Ok(result.xml);
    }
}

接口 Interface

public interface ITicketRepository
  {
      (bool result, string msg, string xml) Test();
  }

业务逻辑仓储

业务逻辑
调用统一下单,获得下单结果
从统一下单成功返回的数据中获取微信浏览器调起jsapi支付所需的参数

public class TicketRepository : ITicketRepository
{
    ILogger<TicketRepository> _logger;
    Tools.WXHelper _wxHelper;

    public TicketRepository(ILogger<TicketRepository> logger, WXHelper wxHelper)
    {
        _logger = logger;
        _wxHelper = wxHelper;
    }

    public (bool result, string msg, string xml) Test()
    {
        _logger.LogWarning("Test comming");
        var wxJsApiParam = "";
        try
        {
            JsApiUnifiedorderDto jsApi = new JsApiUnifiedorderDto()
            {
                body = "ticket",
                total_fee = 0.01,
                openid = "用户的openid",  //我的项目中是直接取得已经记录下来的用户的openid,如果业务中没有记录,需要通过网页授权的接口获取
                out_trade_no = WXHelper.GenerateOutTradeNo(),
                notify_url = $"xxx";  //支付成功回调地址
            };
            //JSAPI支付预处理
            //调用统一下单,获得下单结果
            var unifiedOrderResult = _wxHelper.GetUnifiedOrderResult(jsApi);
            if (unifiedOrderResult.result == false)
                return (false, unifiedOrderResult.msg, null);

            //从统一下单成功返回的数据中获取微信浏览器调起jsapi支付所需的参数
            wxJsApiParam = _wxHelper.GetJsApiParameters(); //获取到的是json格式字符串

            _logger.LogWarning($"Test wxJsApiParam = {wxJsApiParam}");

            JObject jsonObj = JObject.Parse(wxJsApiParam);    //把json字符串转换成json对象
            //这里可以写一些业务逻辑
        }
        catch (Exception e)
        {
            throw new Exception(e.Message);
        }
        return (true, "success", wxJsApiParam);
    }
}

项目中用到的实体

    public class JsApiUnifiedorderDto
    {
        public string body { get; set; }
        public string out_trade_no { get; set; }
        public int total_fee { get; set; }
        public string notify_url { get; set; }
        public string openid { get; set; }
     }

项目中用到的帮助方法

WXHelper
public class WXHelper
{
    ILogger<WXHelper> _logger;
    WebHelper _webHelper;

    public WXHelper(ILogger<WXHelper> logger, WebHelper webHelper)
    {
        _logger = logger;
        _webHelper = webHelper;
    }

    public (bool result, string msg, WxPayData data) GetUnifiedOrderResult(JsApiUnifiedorderDto dto)
    {
        _logger.LogWarning($"GetUnifiedOrderResult comming");
        //统一下单
        WxPayData data = new WxPayData();
        data.SetValue("body", dto.body);
        //data.SetValue("attach", attachStr);
        data.SetValue("out_trade_no", dto.out_trade_no);
        data.SetValue("total_fee", dto.total_fee);
        data.SetValue("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
        data.SetValue("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
        //data.SetValue("goods_tag", "");
        data.SetValue("trade_type", "JSAPI");
        data.SetValue("openid", dto.openid);
        data.SetValue("notify_url", dto.notify_url);

        WxPayData result = UnifiedOrder(data);
        if (!result.IsSet("appid") || !result.IsSet("prepay_id") || result.GetValue("prepay_id").ToString() == "")
        {
            string msg = "";
            if (result.GetValue("err_code").ToString() == "ORDERPAID")
                msg = "该订单已支付!";
            else
                msg = "请稍后再试!";
            _logger.LogError("UnifiedOrder response error!");
            return (false, msg, null);
        }

        unifiedOrderResult = result;
        return (true, "success", result);
    }

    public WxPayData UnifiedOrder(WxPayData inputObj, int timeOut = 6)
    {
        _logger.LogWarning($"UnifiedOrder comming");
        string url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        //检测必填参数
        if (!inputObj.IsSet("out_trade_no"))
        {
            throw new Exception("缺少统一支付接口必填参数out_trade_no!");
        }
        else if (!inputObj.IsSet("body"))
        {
            throw new Exception("缺少统一支付接口必填参数body!");
        }
        else if (!inputObj.IsSet("total_fee"))
        {
            throw new Exception("缺少统一支付接口必填参数total_fee!");
        }
        else if (!inputObj.IsSet("trade_type"))
        {
            throw new Exception("缺少统一支付接口必填参数trade_type!");
        }

        //关联参数
        if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid"))
        {
            throw new Exception("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
        }
        if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id"))
        {
            throw new Exception("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");
        }

        //异步通知url未设置,则使用配置文件中的url
        if (!inputObj.IsSet("notify_url"))
        {
            inputObj.SetValue("notify_url", "xxx");//异步通知url
        }

        inputObj.SetValue("appid", "xxx");//公众账号ID
        inputObj.SetValue("mch_id", "xxx");//商户号
        inputObj.SetValue("spbill_create_ip", "8.8.8.8");//终端ip	  	    
        inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_MD5);//签名类型

        //签名
        inputObj.SetValue("sign", inputObj.MakeSign());
        string xml = inputObj.ToXml();

        var start = DateTime.Now;
        string response = _webHelper.Post(xml, url, false, timeOut);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);

        WxPayData result = new WxPayData();
        result.FromXml(response);
        //ReportCostTime(url, timeCost, result);//测速上报

        return result;
    }

    public string GetJsApiParameters()
    {
        _logger.LogWarning("GetJsApiParameters comming...");

        WxPayData jsApiParam = new WxPayData();
        jsApiParam.SetValue("appId", unifiedOrderResult.GetValue("appid"));
        jsApiParam.SetValue("timeStamp", GenerateTimeStamp());
        jsApiParam.SetValue("nonceStr", GenerateNonceStr());
        jsApiParam.SetValue("package", "prepay_id=" + unifiedOrderResult.GetValue("prepay_id"));
        jsApiParam.SetValue("signType", "MD5");
        jsApiParam.SetValue("paySign", jsApiParam.MakeSign());

        string parameters = jsApiParam.ToJson();
        _logger.LogWarning($"Get jsApiParam : {parameters}");
        return parameters;
    }

    /// <summary>
    /// 根据当前系统时间加随机序列来生成订单号
    /// </summary>
    /// <returns></returns>
    public static string GenerateOutTradeNo()
    {
        var ran = new Random();
        return string.Format("{0}{1}{2}", "1582768421", DateTime.Now.ToString("yyyyMMddHHmmss"), ran.Next(999));
    }

    /// <summary>
    /// 统一下单接口返回结果
    /// </summary>
    public WxPayData unifiedOrderResult { get; set; }

    /// <summary>
    /// 生成随机串,随机串包含字母或数字
    /// </summary>
    /// <returns></returns>
    public static string GenerateNonceStr()
    {
        return Guid.NewGuid().ToString().Replace("-", "");
    }

    /// <summary>
    /// 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数
    /// </summary>
    /// <returns></returns>
    public static string GenerateTimeStamp()
    {
        TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
        return Convert.ToInt64(ts.TotalSeconds).ToString();
    }
}
WxPayData
public class WxPayData
{
    public const string SIGN_TYPE_MD5 = "MD5";
    public const string SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256";
    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)
        {
            //_logger.LogError(this.GetType().ToString(), "WxPayData数据为空!");
            throw new Exception("WxPayData数据为空!");
        }

        string xml = "<xml>";
        foreach (KeyValuePair<string, object> pair in m_values)
        {
            //字段值不能为null,会影响后续流程
            if (pair.Value == null)
            {
                //_logger.LogError(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new Exception("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类型不能含有其他数据类型
            {
                //_logger.LogError(this.GetType().ToString(), "WxPayData字段数据类型错误!");
                throw new Exception("WxPayData字段数据类型错误!");
            }
        }
        xml += "</xml>";
        return xml;
    }

    /**
    * @将xml转为WxPayData对象并返回对象内部的数据
    * @param string 待转换的xml串
    * @return 经转换得到的Dictionary
    * @throws WxPayException
    */
    public SortedDictionary<string, object> FromXml(string xml)
    {
        if (string.IsNullOrEmpty(xml))
        {
            //_logger.LogError(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
            throw new Exception("将空的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"].ToString() != "SUCCESS")
            {
                return m_values;
            }
            CheckSign();//验证签名,不通过会抛异常
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }

        return m_values;
    }

    public SortedDictionary<string, object> NoSignFromXml(string xml)
    {
        if (string.IsNullOrEmpty(xml))
        {
            //_logger.LogError(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
            throw new Exception("将空的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内部的数据中
        }

        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)
            {
                //_logger.LogError(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new Exception("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)
            {
                //_logger.LogError(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new Exception("WxPayData内部含有值为null的字段!");
            }


            str += string.Format("{0}={1}\n", pair.Key, pair.Value.ToString());
        }
        str = HttpUtility.HtmlEncode(str);
        //_logger.LogDebug(this.GetType().ToString(), "Print in Web Page : " + str);
        return str;
    }

    /**
    * @生成签名,详见签名生成算法
    * @return 签名, sign字段不参加签名
    */
    public string MakeSign()
    {
        //转url格式
        string str = ToUrl();
        //在string后加入API KEY
        str += "&key=" + "xxx"; 商户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();
    }

    public bool CheckSign()
    {
        //如果没有设置签名,则跳过检测
        if (!IsSet("sign"))
        {
            //_logger.LogError(this.GetType().ToString(), "WxPayData签名存在但不合法!");
            throw new Exception("WxPayData签名存在但不合法!");
        }
        //如果设置了签名但是签名为空,则抛异常
        else if (GetValue("sign") == null || GetValue("sign").ToString() == "")
        {
            //_logger.LogError(this.GetType().ToString(), "WxPayData签名存在但不合法!");
            throw new Exception("WxPayData签名存在但不合法!");
        }

        //获取接收到的签名
        string return_sign = GetValue("sign").ToString();

        //在本地计算新的签名
        string cal_sign = MakeSign();

        if (cal_sign == return_sign)
        {
            return true;
        }

        //_logger.LogError(this.GetType().ToString(), "WxPayData签名验证错误!");
        throw new Exception("WxPayData签名验证错误!");
    }

一些说明...

帮助类啥的很多都是用的微信官方的SDK里的
最近公司刚好在做公众号的开发,需要有公众号支付,想着记录下来
自己可以再熟悉一下流程,也可以分享一下,后期如果再遇到也方便查看
第一次写这种文章,还有很多不足,如果你刚好看到这个,还望体谅~

下篇:支付成功回调


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM