【原創分享·支付寶支付】HBuilder打包APP調用支付寶客戶端支付

前言 

  最近有點空餘時間,所以,就研究了一下APP支付。前面很早就搞完APP的微信支付了,但是由於時間上和應用上的情況,支付寶一直沒空去研究。然後等我空了的時候,發現支付寶居然升級了支付邏輯,雖然目前還兼容老的方法,但是新的既然出來了,肯定研究新的了。但是網上幾乎都是舊的方法,所以,唯有自己看官方的文檔,慢慢一步一步研究了。在研究的過程中,發現,他跟微信支付的差別蠻大的。好了廢話不多說了,下面直接來乾貨。

  •         首先,你得去螞蟻金服開放平臺申請一個應用,地址:https://openhome.alipay.com註冊一個應用,如下圖:
 
   應用申請下來之後,需要申請功能,我們這裏用到的是“APP支付”功能。如下圖:
 
 
  如果需要查看相關的文檔,那就點擊“APP支付”就可以跳轉到相關的文檔,這裏我直接給出APP需要看到的文檔,地址:https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.J9S7XU&treeId=204&articleId=105465&docType=1 如下圖:
 
 
  我們在編寫服務端的時候,需要用到兩個參數,一個是APPID,這個上面的圖裏面有,還有一個就是密鑰,這個是通過簽名工具生成,可以通過下面這個地址下載工具,然後生成,地址:https://doc.open.alipay.com/doc2/detail.htm?treeId=200&articleId=105351&docType=1
 
 
 
  下載這個工具,然後解壓,雙擊“支付寶RAS密鑰生成器SHAwithRSA1024_V1.0.bat”生成即可,這裏要注意:TIPS:工具不支持含中文或空格的路徑,請下載到英文目錄下使用。
打開工具後,如下圖:
 
 
  先“生成密鑰”,然後再複製公鑰,然後把公鑰複製到平臺,如下圖:
 
 
 
  再保存,如下圖:
 
 
  然後再驗證公鑰的正確性,這裏,可以寫個小工具來驗證,方法如下:
        /// <summary>
        /// 測試公鑰是否對
        /// </summary>
        /// <returns></returns>
        public string testsign()
        {
            string privtekey = Config.privtekey;//這個就是生成器裏面的那個私鑰,第一個大框框那裏的.
            string data = "a=123";//平臺上提供的串
            string sign = RSAFromPkcs8.sign(data, privtekey, "utf-8");
            return sign;
        }
然後再把這個sign的值,複製出來,然後再點擊“驗證公鑰正確性”,如下圖:
 
 
  然後輸入你的“sign”的值:
 

 
 
  點擊“驗證”後,如果提示驗證通過,那麼你這個簽名的方式就是對了,如下圖:
 
 
 
  再點擊“保存”即可。
接下來,我就寫一下服務端生成相應的串的方法,全部貼出來,方便大家模仿吧,其實大家按照下面這個圖,慢慢研究,也可以的,如下圖:
 
 
 
  最後,我們要給回到APP的參數是這個,只要我們按照規則返回即可。下面,我把方法貼出:
 
    public class AliPayController : Controller
    {
        public Dictionary<stringstring> PayInfo = new Dictionary<stringstring>();
        //
        // GET: /AliPay/
        public ActionResult Index()
        {
            testsign();
            GetPayInfo("0.01");
            return View();
        }
 
        /// <summary>
        /// 測試公鑰是否對
        /// </summary>
        /// <returns></returns>
        public string testsign()
        {
            string privtekey = Config.privtekey;//這個就是生成器裏面的那個私鑰,第一個大框框那裏的.
            string data = "a=123";//平臺上提供的串
            string sign = RSAFromPkcs8.sign(data, privtekey, "utf-8");
            return sign;
        }
 
 
        /// <summary>
        /// 獲取支付信息
        /// </summary>
        /// <param name="_amount"></param>
        /// <returns></returns>
        public string GetPayInfo(string _amount)//_amount:付款金額
        {
            string strJson = string.Empty;
            try
            {
                string orderInfo = GetOrderInfoWithOutEncode(_amount);
                // 對訂單做RSA 簽名
                string sign = RSAFromPkcs8.sign(orderInfo, Config.privtekey, "utf-8");
                //僅需對sign做URL編碼
                sign = HttpUtility.UrlEncode(sign, Encoding.UTF8);
                string payInfo = GetOrderInfoWithEncode() + "&sign=" + sign;
                strJson = payInfo.Replace("+""%20");//日期那裏會有一個空格(2017-01-05 11:11:11)轉化爲+,所以這裏要替換一下
                FileLog.WriteLog("支付寶串:" + strJson);
            }
            catch (Exception ex)
            {
                FileLog.WriteLog(ex.ToString());
 
            }
            return strJson;
        }
        /// <summary>
        /// 不包含Encode的字符串拼接
        /// </summary>
        /// <param name="price"></param>
        /// <returns></returns>
        public string GetOrderInfoWithOutEncode(string price)
        {
            PayInfo.Add("app_id"Config.app_id);
            PayInfo.Add("biz_content", GetBizContent(price));
            PayInfo.Add("charset""utf-8");
            PayInfo.Add("format""json");
            PayInfo.Add("method""alipay.trade.app.pay");
            PayInfo.Add("notify_url""http://wxpay.lmx.ren/ResultNotify");
            PayInfo.Add("sign_type""RSA");
            PayInfo.Add("timestamp"DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            PayInfo.Add("version""1.0");
            string strUrl = BuildQueryWithOutEncode(PayInfo);
            return strUrl;
        }
        /// <summary>
        /// 包含Encode的字符串拼接
        /// </summary>
        /// <param name="price"></param>
        /// <returns></returns>
        public string GetOrderInfoWithEncode()
        {
            string strUrl = BuildQuery(PayInfo, "utf-8");
            return strUrl;
        }
        /// <summary>
        /// 獲取支付內容詳情
        /// </summary>
        /// <param name="total_amount"></param>
        /// <returns></returns>
        public string GetBizContent(string total_amount)
        {
            Dictionary<stringstring> biz_content_info = new Dictionary<stringstring>();
            biz_content_info.Add("timeout_express""30m");//該筆訂單允許的最晚付款時間,逾期將關閉交易。
            biz_content_info.Add("seller_id""");//收款支付寶用戶ID。 如果該值爲空,則默認爲商戶簽約賬號對應的支付寶用戶ID
            biz_content_info.Add("product_code""QUICK_MSECURITY_PAY");//銷售產品碼,商家和支付寶簽約的產品碼,爲固定值QUICK_MSECURITY_PAY
            biz_content_info.Add("total_amount""0.01");//訂單總金額,單位爲元,精確到小數點後兩位,取值範圍[0.01,100000000]
            biz_content_info.Add("subject""Iphone7 128G");//商品的標題/交易標題/訂單標題/訂單關鍵字等。
            biz_content_info.Add("body""最新款的手機啦");//對一筆交易的具體描述信息。如果是多種商品,請將商品描述字符串累加傳給body。
            biz_content_info.Add("out_trade_no"DateTime.Now.ToString("yyyyMMddHHmmssffffff"));//商戶網站唯一訂單號
            string strBizContent = JsonHelper.Serialize(biz_content_info);
            return strBizContent;
        }
 
 
        /// <summary>
        /// 組裝普通文本請求參數(帶Encode)。
        /// </summary>
        /// <param name="parameters">Key-Value形式請求參數字典</param>
        /// <returns>URL編碼後的請求數據</returns>
        public static string BuildQuery(IDictionary<stringstring> parameters, string charset)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;
 
            IEnumerator<KeyValuePair<stringstring>> dem = parameters.GetEnumerator();
            while (dem.MoveNext())
            {
                string name = dem.Current.Key;
                string value = dem.Current.Value;
                // 忽略參數名或參數值爲空的參數
                if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(value))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
 
                    postData.Append(name);
                    postData.Append("=");
 
                    string encodedValue = HttpUtility.UrlEncode(value, Encoding.GetEncoding(charset));
 
                    postData.Append(encodedValue);
                    hasParam = true;
                }
            }
            return postData.ToString();
        }
        /// <summary>
        /// 組裝普通文本請求參數(不帶Encode)。
        /// </summary>
        /// <param name="parameters">Key-Value形式請求參數字典</param>
        /// <returns>URL編碼後的請求數據</returns>
        public static string BuildQueryWithOutEncode(IDictionary<stringstring> parameters)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;
 
            IEnumerator<KeyValuePair<stringstring>> dem = parameters.GetEnumerator();
            while (dem.MoveNext())
            {
                string name = dem.Current.Key;
                string value = dem.Current.Value;
                // 忽略參數名或參數值爲空的參數
                if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(value))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
 
                    postData.Append(name);
                    postData.Append("=");
 
                    string encodedValue = value;
                    postData.Append(encodedValue);
                    hasParam = true;
                }
            }
            return postData.ToString();
        }
        /// <summary>
        /// 配置(請自行填上下面兩個參數)
        /// </summary>
        public class Config
        {
            /// <summary>
            /// 應用APPID
            /// </summary>
            public const string app_id = "";
            /// <summary>
            /// 私鑰,通過工具生成
            /// </summary>
            public const string privtekey = "";
        }
 
    }
 
  然後還有一個簽名的文件,代碼如下:
 
    /// <summary>
    /// 類名:RSAFromPkcs8
    /// 功能:RSA解密、簽名、驗籤
    /// 詳細:該類對Java生成的密鑰進行解密和簽名以及驗籤專用類,不需要修改
    /// 版本:2.0
    /// 修改日期:2011-05-10
    /// 說明:
    /// 以下代碼只是爲了方便商戶測試而提供的樣例代碼,商戶可以根據自己網站的需要,按照技術文檔編寫,並非一定要使用該代碼。
    /// 該代碼僅供學習和研究支付寶接口使用,只是提供一個參考。
    /// </summary>
    public sealed class RSAFromPkcs8
    {
        /// <summary>
        /// 簽名
        /// </summary>
        /// <param name="content">需要簽名的內容</param>
        /// <param name="privateKey">私鑰</param>
        /// <param name="input_charset">編碼格式</param>
        /// <returns></returns>
        public static string sign(string content, string privateKey, string input_charset)
        {
            Encoding code = Encoding.GetEncoding(input_charset);
            byte[] Data = code.GetBytes(content);
            RSACryptoServiceProvider rsa = DecodePemPrivateKey(privateKey);
            SHA1 sh = new SHA1CryptoServiceProvider();
            byte[] signData = rsa.SignData(Data, sh);
            return Convert.ToBase64String(signData);
        }
 
        /// <summary>
        /// 驗證簽名
        /// </summary>
        /// <param name="content">需要驗證的內容</param>
        /// <param name="signedString">簽名結果</param>
        /// <param name="publicKey">公鑰</param>
        /// <param name="input_charset">編碼格式</param>
        /// <returns></returns>
        public static bool verify(string content, string signedString, string publicKey, string input_charset)
        {
            bool result = false;
 
            Encoding code = Encoding.GetEncoding(input_charset);
            byte[] Data = code.GetBytes(content);
            byte[] data = Convert.FromBase64String(signedString);
            RSAParameters paraPub = ConvertFromPublicKey(publicKey);
            RSACryptoServiceProvider rsaPub = new RSACryptoServiceProvider();
            rsaPub.ImportParameters(paraPub);
 
            SHA1 sh = new SHA1CryptoServiceProvider();
            result = rsaPub.VerifyData(Data, sh, data);
            return result;
        }
 
        /// <summary>
        /// 用RSA解密
        /// </summary>
        /// <param name="resData">待解密字符串</param>
        /// <param name="privateKey">私鑰</param>
        /// <param name="input_charset">編碼格式</param>
        /// <returns>解密結果</returns>
        public static string decryptData(string resData, string privateKey, string input_charset)
        {
            byte[] DataToDecrypt = Convert.FromBase64String(resData);
            List<byte> result = new List<byte>();
 
            for (int j = 0; j < DataToDecrypt.Length / 128; j++)
            {
                byte[] buf = new byte[128];
                for (int i = 0; i < 128; i++)
                {
                    buf[i] = DataToDecrypt[i + 128 * j];
                }
                result.AddRange(decrypt(buf, privateKey, input_charset));
            }
            byte[] source = result.ToArray();
            char[] asciiChars = new char[Encoding.GetEncoding(input_charset).GetCharCount(source, 0, source.Length)];
            Encoding.GetEncoding(input_charset).GetChars(source, 0, source.Length, asciiChars, 0);
            return new string(asciiChars);
        }
 
        private static byte[] decrypt(byte[] data, string privateKey, string input_charset)
        {
            RSACryptoServiceProvider rsa = DecodePemPrivateKey(privateKey);
            SHA1 sh = new SHA1CryptoServiceProvider();
            return rsa.Decrypt(data, false);
        }
 
        /// <summary>
        /// 解析java生成的pem文件私鑰
        /// </summary>
        /// <param name="pemstr"></param>
        /// <returns></returns>
        private static RSACryptoServiceProvider DecodePemPrivateKey(String pemstr)
        {
            byte[] pkcs8privatekey;
            pkcs8privatekey = Convert.FromBase64String(pemstr);
            if (pkcs8privatekey != null)
            {
 
                RSACryptoServiceProvider rsa = DecodePrivateKeyInfo(pkcs8privatekey);
                return rsa;
            }
            else
                return null;
        }
 
        private static RSACryptoServiceProvider DecodePrivateKeyInfo(byte[] pkcs8)
        {
 
            byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
            byte[] seq = new byte[15];
 
            MemoryStream mem = new MemoryStream(pkcs8);
            int lenstream = (int)mem.Length;
            BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
 
            try
            {
 
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)    //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();    //advance 2 bytes
                else
                    return null;
 
 
                bt = binr.ReadByte();
                if (bt != 0x02)
                    return null;
 
                twobytes = binr.ReadUInt16();
 
                if (twobytes != 0x0001)
                    return null;
 
                seq = binr.ReadBytes(15);        //read the Sequence OID
                if (!CompareBytearrays(seq, SeqOID))    //make sure Sequence for OID is correct
                    return null;
 
                bt = binr.ReadByte();
                if (bt != 0x04)    //expect an Octet string 
                    return null;
 
                bt = binr.ReadByte();        //read next byte, or next 2 bytes is  0x81 or 0x82; otherwise bt is the byte count
                if (bt == 0x81)
                    binr.ReadByte();
                else
                    if (bt == 0x82)
                        binr.ReadUInt16();
                //------ at this stage, the remaining sequence should be the RSA private key
 
                byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
                RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey(rsaprivkey);
                return rsacsp;
            }
 
            catch (Exception)
            {
                return null;
            }
 
            finally { binr.Close(); }
 
        }
 
 
        private static bool CompareBytearrays(byte[] a, byte[] b)
        {
            if (a.Length != b.Length)
                return false;
            int i = 0;
            foreach (byte c in a)
            {
                if (c != b[i])
                    return false;
                i++;
            }
            return true;
        }
 
        private static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
        {
            byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
 
            // ---------  Set up stream to decode the asn.1 encoded RSA private key  ------
            MemoryStream mem = new MemoryStream(privkey);
            BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
            int elems = 0;
            try
            {
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)    //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();    //advance 2 bytes
                else
                    return null;
 
                twobytes = binr.ReadUInt16();
                if (twobytes != 0x0102)    //version number
                    return null;
                bt = binr.ReadByte();
                if (bt != 0x00)
                    return null;
 
 
                //------  all private key components are Integer sequences ----
                elems = GetIntegerSize(binr);
                MODULUS = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                E = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                D = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                P = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                Q = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                DP = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                DQ = binr.ReadBytes(elems);
 
                elems = GetIntegerSize(binr);
                IQ = binr.ReadBytes(elems);
 
                // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                RSAParameters RSAparams = new RSAParameters();
                RSAparams.Modulus = MODULUS;
                RSAparams.Exponent = E;
                RSAparams.D = D;
                RSAparams.P = P;
                RSAparams.Q = Q;
                RSAparams.DP = DP;
                RSAparams.DQ = DQ;
                RSAparams.InverseQ = IQ;
                RSA.ImportParameters(RSAparams);
                return RSA;
            }
            catch (Exception)
            {
                return null;
            }
            finally { binr.Close(); }
        }
 
        private static int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)        //expect integer
                return 0;
            bt = binr.ReadByte();
 
            if (bt == 0x81)
                count = binr.ReadByte();    // data size in next byte
            else
                if (bt == 0x82)
                {
                    highbyte = binr.ReadByte();    // data size in next 2 bytes
                    lowbyte = binr.ReadByte();
                    byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                    count = BitConverter.ToInt32(modint, 0);
                }
                else
                {
                    count = bt;        // we already have the data size
                }
 
 
 
            while (binr.ReadByte() == 0x00)
            {    //remove high order zeros in data
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);        //last ReadByte wasn't a removed zero, so back up a byte
            return count;
        }
 
        #region 解析.net 生成的Pem
        private static RSAParameters ConvertFromPublicKey(string pemFileConent)
        {
 
            byte[] keyData = Convert.FromBase64String(pemFileConent);
            if (keyData.Length < 162)
            {
                throw new ArgumentException("pem file content is incorrect.");
            }
            byte[] pemModulus = new byte[128];
            byte[] pemPublicExponent = new byte[3];
            Array.Copy(keyData, 29, pemModulus, 0, 128);
            Array.Copy(keyData, 159, pemPublicExponent, 0, 3);
            RSAParameters para = new RSAParameters();
            para.Modulus = pemModulus;
            para.Exponent = pemPublicExponent;
            return para;
        }
 
        private static RSAParameters ConvertFromPrivateKey(string pemFileConent)
        {
            byte[] keyData = Convert.FromBase64String(pemFileConent);
            if (keyData.Length < 609)
            {
                throw new ArgumentException("pem file content is incorrect.");
            }
 
            int index = 11;
            byte[] pemModulus = new byte[128];
            Array.Copy(keyData, index, pemModulus, 0, 128);
 
            index += 128;
            index += 2;//141
            byte[] pemPublicExponent = new byte[3];
            Array.Copy(keyData, index, pemPublicExponent, 0, 3);
 
            index += 3;
            index += 4;//148
            byte[] pemPrivateExponent = new byte[128];
            Array.Copy(keyData, index, pemPrivateExponent, 0, 128);
 
            index += 128;
            index += ((int)keyData[index + 1] == 64 ? 2 : 3);//279
            byte[] pemPrime1 = new byte[64];
            Array.Copy(keyData, index, pemPrime1, 0, 64);
 
            index += 64;
            index += ((int)keyData[index + 1] == 64 ? 2 : 3);//346
            byte[] pemPrime2 = new byte[64];
            Array.Copy(keyData, index, pemPrime2, 0, 64);
 
            index += 64;
            index += ((int)keyData[index + 1] == 64 ? 2 : 3);//412/413
            byte[] pemExponent1 = new byte[64];
            Array.Copy(keyData, index, pemExponent1, 0, 64);
 
            index += 64;
            index += ((int)keyData[index + 1] == 64 ? 2 : 3);//479/480
            byte[] pemExponent2 = new byte[64];
            Array.Copy(keyData, index, pemExponent2, 0, 64);
 
            index += 64;
            index += ((int)keyData[index + 1] == 64 ? 2 : 3);//545/546
            byte[] pemCoefficient = new byte[64];
            Array.Copy(keyData, index, pemCoefficient, 0, 64);
 
            RSAParameters para = new RSAParameters();
            para.Modulus = pemModulus;
            para.Exponent = pemPublicExponent;
            para.D = pemPrivateExponent;
            para.P = pemPrime1;
            para.Q = pemPrime2;
            para.DP = pemExponent1;
            para.DQ = pemExponent2;
            para.InverseQ = pemCoefficient;
            return para;
        }
        #endregion
 

    }

 
  服務端的完整代碼就如上了。
下面我吧HBuilder裏面的代碼也寫一下,就是選擇好“支付寶”之後,執行的代碼是:
 
                    plus.nativeUI.showWaiting();
                    mui.post("http://wxpay.lmx.ren/AliPay/GetPayInfo", {
                        _amount: 0.01
                    }, function(data) {
                        plus.nativeUI.closeWaiting();
                        if (data) {
                            plus.payment.request(payChanel, data, function(result) {
                                console.log(JSON.stringify(result));
                                mui.alert(JSON.stringify(result), title);
                                mui.alert("付費成功", title);
                            }, function(e) {
                                console.log(JSON.stringify(e));
                                alert(JSON.stringify(e));
                                mui.alert("付費失敗", title);
                            });
                        } else {
                            plus.nativeUI.alert("支付失敗");
                        }
                    });

   好了,就是如此簡單。下面貼幾張成功的圖片,方便大家預覽。
 
 
 
  好了,這次教程到此結束。如果代碼有漏的,回覆評論,我會上來看。如果需要討論的,加羣討論,QQ個人好友已滿,加不了了,抱歉。
 
 
      到這裏,就大功告成啦,接下來的東西,就由大家自己去展開拓展了,本次經驗分享到此結束,寫過博客的人都知道,好好寫一個博客,需要自己從頭重新走一遍代碼,所以,各種辛苦,只有自己能體會。所以您如果覺得寫得不錯,或者對你有幫助,請點“好文要頂”或者“關注我”,順帶也可以評論一兩句,大家互相交流交流,轉載請保留原作者地址以及姓名
 
 
 
我新建一個QQ羣,如果有問題,可以在羣裏提。如果合適,也會根據大家提的比較多的問題,來寫篇博文,幫助更多的人,羣號:275523437
點擊鏈接加入羣【.Net,MVC,EasyUI,MUI,Html,JS】http://jq.qq.com/?_wv=1027&k=2A0RbLd
 
 
(如果有私活,或者一起合作的,也可以私信找我呀,嘿嘿);
 
 
作者:南宮蕭塵  
QQ:314791147
日期:2017-01-05

 
需要實時測試的,可以關注公衆號,測試相關功能(根據實際情況,可能會不定時更新程序,如果需要最新程序的,可以加羣聯繫,QQ羣號在上面):
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章