【.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