C# 解析 URL URI 中的参数

C# 解析 URL URI 中的参数

完整代码




namespace System
{
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Text;

    /// <summary>
    /// 函数<see cref="GetQueryDictionary(string, bool)"/> 和 函数<see cref="GetQueryCollection(string, bool)"/><br />
    /// 支持解析示例:<br />
    /// <![CDATA[    https://www.google.com/index?page=1&lang=eng    ]]> <br />
    /// <![CDATA[    https://www.google.com/    ]]> <br />
    /// <![CDATA[    https://www.google.com/index?    ]]> <br />
    /// <![CDATA[    https://www.google.com/index?page=title=index=1&lang=&chang&char=?&id=123    ]]> <br />
    /// </summary>
    public static class UrlHelper
    {
        #region Test 测试示例。

        /// <summary>
        /// 使用示例。
        /// </summary>
        [System.Diagnostics.Conditional("DEBUG")]
        public static void Test()
        {
            TestGetQueryNormal();
            TestGetQuerySpecial();
            TestGetUrlStringNormal();
        }

        /// <summary>
        /// 常见的使用示例。解析URL中的参数。
        /// </summary>
        [System.Diagnostics.Conditional("DEBUG")]
        public static void TestGetQueryNormal()
        {
            string urlTest1 = "https://www.google.com/index?page=1&lang=eng";

            string dictionaryBaseUrlTest1;
            var dictionaryTest1 = GetQueryDictionary(urlTest1, out dictionaryBaseUrlTest1);
            System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest1 == "https://www.google.com/index" &&
                dictionaryTest1["page"] == "1" && dictionaryTest1["lang"] == "eng",
                "GetQueryDictionary Test1");
            System.Diagnostics.Debug.Assert(GetUrlString(dictionaryBaseUrlTest1, dictionaryTest1) == urlTest1,
                "GetUrlString by Dictionary Test1");

            string collectionBaseUrlTest1;
            var collectionTest1 = GetQueryCollection(urlTest1, out collectionBaseUrlTest1);
            System.Diagnostics.Debug.Assert(collectionBaseUrlTest1 == "https://www.google.com/index" &&
                collectionTest1["page"] == "1" && collectionTest1["lang"] == "eng",
                "GetQueryCollection Test1");
            System.Diagnostics.Debug.Assert(GetUrlString(collectionBaseUrlTest1, collectionTest1) == urlTest1,
                "GetUrlString by Collection Test1");

            string urlTest2 = "https://www.google.com/";

            string dictionaryBaseUrlTest2;
            GetQueryDictionary(urlTest2, out dictionaryBaseUrlTest2);
            System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest2 == "https://www.google.com/",
                "GetQueryDictionary Test2");
            string collectionBaseUrlTest2;
            GetQueryCollection(urlTest2, out collectionBaseUrlTest2);
            System.Diagnostics.Debug.Assert(collectionBaseUrlTest2 == "https://www.google.com/",
                "GetQueryCollection Test2");

            string urlTest3 = "https://www.google.com/index?";

            string dictionaryBaseUrlTest3;
            GetQueryDictionary(urlTest3, out dictionaryBaseUrlTest3);
            System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest3 == "https://www.google.com/index",
                "GetQueryDictionary Test3");

            string collectionBaseUrlTest3;
            GetQueryCollection(urlTest3, out collectionBaseUrlTest3);
            System.Diagnostics.Debug.Assert(collectionBaseUrlTest3 == "https://www.google.com/index",
                "GetQueryCollection Test4");

        }

        /// <summary>
        /// 不常见的使用示例。解析URL中的参数。
        /// </summary>
        [System.Diagnostics.Conditional("DEBUG")]
        public static void TestGetQuerySpecial()
        {
            string urlSpecial4 = "https://www.google.com/index?page=title=index=1&lang=&chang&char=?&id=123";

            string dictionaryBaseUrlSpecial4;
            var dictionarySpecial4 = GetQueryDictionary(urlSpecial4, out dictionaryBaseUrlSpecial4);
            System.Diagnostics.Debug.Assert(dictionaryBaseUrlSpecial4 == "https://www.google.com/index" &&
                dictionarySpecial4["page"] == "title=index=1" && dictionarySpecial4["lang"] == "" &&
                dictionarySpecial4["chang"] == null && dictionarySpecial4["char"] == "?" && dictionarySpecial4["id"] == "123",
                "GetQueryDictionary UrlSpecial4");

            string collectionBaseUrlSpecial4;
            var collectionSpecial4 = GetQueryCollection(urlSpecial4, out collectionBaseUrlSpecial4);
            System.Diagnostics.Debug.Assert(collectionBaseUrlSpecial4 == "https://www.google.com/index" &&
                collectionSpecial4["page"] == "title=index=1" && collectionSpecial4["lang"] == "" &&
                collectionSpecial4["chang"] == null && collectionSpecial4["char"] == "?" && collectionSpecial4["id"] == "123",
                "GetQueryCollection UrlSpecial4");

            string urlSpecial5 = "https://www.google.com/index?page=1&&lang=eng";

            string dictionaryBaseUrlSpecial5;
            var dictionarySpecial5 = GetQueryDictionary(urlSpecial5, out dictionaryBaseUrlSpecial5);
            System.Diagnostics.Debug.Assert(dictionaryBaseUrlSpecial5 == "https://www.google.com/index" &&
                dictionarySpecial5["page"] == "1" && dictionarySpecial5["lang"] == "eng",
                "GetQueryDictionary UrlSpecial5");

            string collectionBaseUrlSpecial5;
            var collectionSpecial5 = GetQueryCollection(urlSpecial5, out collectionBaseUrlSpecial5);
            System.Diagnostics.Debug.Assert(collectionBaseUrlSpecial5 == "https://www.google.com/index" &&
                collectionSpecial5["page"] == "1" && collectionSpecial5["lang"] == "eng",
                "GetQueryCollection UrlSpecial5");
        }

        /// <summary>
        /// 常见的使用示例。拼接URL的<paramref name="baseUrl"/>和URL中的参数。
        /// </summary>
        [System.Diagnostics.Conditional("DEBUG")]
        public static void TestGetUrlStringNormal()
        {
            string baseUrl1 = "https://www.google.com/index?page=1&lang=eng";

            System.Diagnostics.Debug.Assert(GetUrlString(baseUrl1, new Dictionary<string, string>()
            {
                ["TestKey1"] = "TestValue1",
                ["TestKey2"] = "TestValue2",
            }) == "https://www.google.com/index?page=1&lang=eng&TestKey1=TestValue1&TestKey2=TestValue2",
                "GetUrlString by Dictionary BaseUrl1");

            System.Diagnostics.Debug.Assert(GetUrlString(baseUrl1, new NameValueCollection()
            {
                ["TestKey1"] = "TestValue1",
                ["TestKey2"] = "TestValue2",
            }) == "https://www.google.com/index?page=1&lang=eng&TestKey1=TestValue1&TestKey2=TestValue2",
                "GetUrlString by Collection BaseUrl1");

            string baseUrl2 = "https://www.google.com/index";

            System.Diagnostics.Debug.Assert(GetUrlString(baseUrl2, new Dictionary<string, string>()
            {
                ["TestKey1"] = "TestValue1",
                ["TestKey2"] = "TestValue2",
            }) == "https://www.google.com/index?TestKey1=TestValue1&TestKey2=TestValue2",
                "GetUrlString by Dictionary BaseUrl2");

            System.Diagnostics.Debug.Assert(GetUrlString(baseUrl2, new NameValueCollection()
            {
                ["TestKey1"] = "TestValue1",
                ["TestKey2"] = "TestValue2",
            }) == "https://www.google.com/index?TestKey1=TestValue1&TestKey2=TestValue2",
                "GetUrlString by Collection BaseUrl2");
        }

        #endregion Test 测试示例。

        /// <summary>
        /// 解析URL中的参数。会覆盖重复键的值。<br />
        /// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,
        /// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[    this[TKey key]    ]]>,
        /// 直接读取不存在的键值对时,会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="ignoreCase"></param>
        /// <returns></returns>
        public static Dictionary<string, string> GetQueryDictionary(string url, bool ignoreCase = false)
        {
            string baseUrl;
            return GetQueryDictionary(url, out baseUrl, ignoreCase);
        }

        /// <summary>
        /// 解析URL中的参数。会覆盖重复键的值。<br />
        /// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,
        /// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[    this[TKey key]    ]]>,
        /// 直接读取不存在的键值对时,会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="baseUrl"> URL中符号“?”的前面部分。</param>
        /// <param name="ignoreCase"></param>
        /// <returns></returns>
        public static Dictionary<string, string> GetQueryDictionary(string url, out string baseUrl, bool ignoreCase = false)
        {
            StringComparer comparer;
            if (ignoreCase)
            {
                comparer = StringComparer.InvariantCultureIgnoreCase;
            }
            else
            {
                comparer = StringComparer.InvariantCulture;
            }
            return GetQueryDictionary(url, out baseUrl, comparer);
        }

        /// <summary>
        /// 解析URL中的参数。会覆盖重复键的值。<br />
        /// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,
        /// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[    this[TKey key]    ]]>,
        /// 直接读取不存在的键值对时,会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="baseUrl"> URL中符号“?”的前面部分。</param>
        /// <param name="comparer"></param>
        /// <returns></returns>
        public static Dictionary<string, string> GetQueryDictionary(string url, out string baseUrl, StringComparer comparer)
        {
            baseUrl = null;

            // 第一个“?”符号的下标。
            // 用于支持,参数中包括“?”符号的URL。
            int indexQuestionMark = url.IndexOf('?');
            int countQuery = url.Length - indexQuestionMark - 1;

            Dictionary<string, string> info = null;

            // 如果URL中包括有效的参数。
            if (indexQuestionMark > -1 && countQuery > 0)
            {
                string queryString = url.Substring(indexQuestionMark + 1, countQuery);

                // 为空的键值对没有意义,所以,舍弃为空的键值对。
                string[] keyAndValuePairs = queryString.Split(new char[] { '&' }/*, StringSplitOptions.RemoveEmptyEntries*/);
                info = new Dictionary<string, string>(keyAndValuePairs.Length + 1, comparer);
                foreach (var pair in keyAndValuePairs)
                {
                    // 第一个“=”符号的下标。
                    // 用于支持,value中包括“=”符号的参数。
                    int indexEquals = pair.IndexOf('=');

                    // 如果包含“=”符号。
                    if (indexEquals > -1)
                    {
                        string key = pair.Substring(0, indexEquals);
                        key = Uri.UnescapeDataString(key);

                        int countValue = pair.Length - indexEquals - 1;
                        // 避免“=”符号后面没有内容时,indexEquals + 1超出数组的有效索引。
                        string value = (countValue == 0) ? string.Empty : pair.Substring(indexEquals + 1, countValue);
                        value = Uri.UnescapeDataString(value);

                        info[key] = value;
                    }
                    // 用关键字保存特殊参数。
                    else
                    {
                        info[pair] = null;
                    }
                }
            }
            info = info ?? new Dictionary<string, string>(1, comparer);

            if (indexQuestionMark < 0 || indexQuestionMark > url.Length)
            {
                indexQuestionMark = url.Length;
            }
            // URL中符号“?”的前面部分。
            baseUrl = url.Substring(0, indexQuestionMark);
            return info;
        }

        /// <summary>
        /// 解析URL中的参数。支持重复键。<br />
        /// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="ignoreCase"></param>
        /// <returns></returns>
        public static NameValueCollection GetQueryCollection(string url, bool ignoreCase = false)
        {
            string baseUrl;
            return GetQueryCollection(url, out baseUrl, ignoreCase);
        }

        /// <summary>
        /// 解析URL中的参数。支持重复键。<br />
        /// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="baseUrl"> URL中符号“?”的前面部分。</param>
        /// <param name="ignoreCase"></param>
        /// <returns></returns>
        public static NameValueCollection GetQueryCollection(string url, out string baseUrl, bool ignoreCase = false)
        {
            StringComparer comparer;
            if (ignoreCase)
            {
                comparer = StringComparer.InvariantCultureIgnoreCase;
            }
            else
            {
                comparer = StringComparer.InvariantCulture;
            }
            return GetQueryCollection(url, out baseUrl, comparer);
        }

        /// <summary>
        /// 解析URL中的参数。支持重复键。<br />
        /// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />
        /// </summary>
        /// <param name="url"></param>
        /// <param name="baseUrl"> URL中符号“?”的前面部分。</param>
        /// <param name="comparer"></param>
        /// <returns></returns>
        public static NameValueCollection GetQueryCollection(string url, out string baseUrl, StringComparer comparer)
        {

            // 第一个“?”符号的下标。
            // 用于支持,参数中包括“?”符号的URL。
            int indexQuestionMark = url.IndexOf('?');
            int countQuery = url.Length - indexQuestionMark - 1;

            NameValueCollection info = null;

            // 如果URL中包括有效的参数。
            if (indexQuestionMark > -1 && countQuery > 0)
            {
                string queryString = url.Substring(indexQuestionMark + 1, countQuery);

                // 为空的键值对没有意义,所以,舍弃为空的键值对。
                string[] keyAndValuePairs = queryString.Split(new char[] { '&' }/*, StringSplitOptions.RemoveEmptyEntries*/);
                info = new NameValueCollection(keyAndValuePairs.Length + 1, comparer);
                foreach (var pair in keyAndValuePairs)
                {
                    // 第一个“=”符号的下标。
                    // 用于支持,value中包括“=”符号的参数。
                    int indexEquals = pair.IndexOf('=');

                    // 如果包含“=”符号。
                    if (indexEquals > -1)
                    {
                        string key = pair.Substring(0, indexEquals);
                        key = Uri.UnescapeDataString(key);

                        int countValue = pair.Length - indexEquals - 1;
                        // 避免“=”符号后面没有内容时,indexEquals + 1超出数组的有效索引。
                        string value = (countValue == 0) ? string.Empty : pair.Substring(indexEquals + 1, countValue);
                        value = Uri.UnescapeDataString(value);

                        info.Add(key, value);
                    }
                    // 用关键字保存特殊参数。
                    else
                    {
                        info.Add(pair, null);
                    }
                }
            }
            info = info ?? new NameValueCollection(1, comparer);

            if (indexQuestionMark < 0 || indexQuestionMark > url.Length)
            {
                indexQuestionMark = url.Length;
            }
            // URL中符号“?”的前面部分。
            baseUrl = url.Substring(0, indexQuestionMark);
            return info;
        }

        /// <summary>
        /// 拼接URL的<paramref name="baseUrl"/>(可以是URL中符号“?”的前面部分,也可以是已经包含参数的URL)和URL中的参数。
        /// </summary>
        /// <param name="baseUrl"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string GetUrlString(string baseUrl, ICollection<KeyValuePair<string, string>> parameters)
        {
            if (parameters != null && parameters.Count > 0)
            {
                var indexEndSplit = baseUrl.LastIndexOf('/');
                if (indexEndSplit < 0)
                {
                    indexEndSplit = 0;
                }
                StringBuilder builder = new StringBuilder(baseUrl.Length + 128 + 1);
                var indexQuestionMark = baseUrl.IndexOf('?', indexEndSplit);
                if (indexQuestionMark < 0)
                {
                    builder.Append(baseUrl);
                    bool hasQueryItem = false;
                    bool addQuestionMark = true;
                    GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
                }
                else
                {
                    builder.Append(baseUrl);
                    bool hasQueryItem = indexQuestionMark < baseUrl.Length - 1;
                    bool addQuestionMark = false;
                    GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
                }
                return builder.ToString();
            }
            return baseUrl;
        }

        /// <summary>
        /// 拼接URL的<paramref name="baseUrl"/>(可以是URL中符号“?”的前面部分,也可以是已经包含参数的URL)和URL中的参数。
        /// </summary>
        /// <param name="baseUrl"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string GetUrlString(string baseUrl, NameValueCollection parameters)
        {
            if (parameters != null && parameters.Count > 0)
            {
                var indexEndSplit = baseUrl.LastIndexOf('/');
                if (indexEndSplit < 0)
                {
                    indexEndSplit = 0;
                }
                StringBuilder builder = new StringBuilder(baseUrl.Length + 128 + 1);
                var indexQuestionMark = baseUrl.IndexOf('?', indexEndSplit);
                if (indexQuestionMark < 0)
                {
                    builder.Append(baseUrl);
                    bool hasQueryItem = false;
                    bool addQuestionMark = true;
                    GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
                }
                else
                {
                    builder.Append(baseUrl);
                    bool hasQueryItem = indexQuestionMark < baseUrl.Length - 1;
                    bool addQuestionMark = false;
                    GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
                }
                return builder.ToString();
            }
            return baseUrl;
        }

        public static string GetUrlQueryString(ICollection<KeyValuePair<string, string>> parameters, bool addQuestionMark)
        {
            StringBuilder builder = new StringBuilder(128);
            bool hasQueryItem = false;
            GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
            return builder.ToString();
        }

        public static string GetUrlQueryString(NameValueCollection parameters, bool addQuestionMark)
        {
            StringBuilder builder = new StringBuilder(128);
            bool hasQueryItem = false;
            GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);
            return builder.ToString();
        }

        private static void GetUrlQueryStringCore(ICollection<KeyValuePair<string, string>> parameters, StringBuilder builder, bool hasQueryItem, bool addQuestionMark)
        {
            foreach (KeyValuePair<string, string> item in parameters)
            {
                GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);
                builder.Append(Uri.EscapeDataString(item.Key));
                if (item.Value != null)
                {
                    builder.Append("=");
                    builder.Append(Uri.EscapeDataString(item.Value));
                }
            }
        }

        private static void GetUrlQueryStringCore(NameValueCollection parameters, StringBuilder builder, bool hasQueryItem, bool addQuestionMark)
        {
            foreach (object itemKeyObj in parameters.Keys)
            {
                string itemKey = itemKeyObj?.ToString();
                if (itemKey != null)
                {
                    var itemValues = parameters.GetValues(itemKey);
                    if (itemValues == null)
                    {
                        GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);
                        builder.Append(Uri.EscapeDataString(itemKey));
                    }
                    else
                    {
                        foreach (var itemValue in itemValues)
                        {
                            GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);
                            builder.Append(Uri.EscapeDataString(itemKey));
                            if (itemValue != null)
                            {
                                builder.Append("=");
                                builder.Append(Uri.EscapeDataString(itemValue));
                            }
                        }
                    }
                }
            }
        }

        private static void GetUrlQueryStringForAddParameterBefore(StringBuilder builder, ref bool hasQueryItem, bool addQuestionMark)
        {
            if (hasQueryItem)
            {
                builder.Append("&");
            }
            else
            {
                hasQueryItem = true;
                if (addQuestionMark)
                {
                    builder.Append('?');
                }
            }
        }

    }
}


概述

核心功能概述

  1. 查询参数解析

    • GetQueryDictionary():将URL查询参数解析为字典(键值唯一,后值覆盖前值)
    • GetQueryCollection():将URL查询参数解析为集合(支持多值键)
  2. URL构建

    • GetUrlString():将基础URL与参数集合/字典拼接成完整URL
  3. 特殊支持

    • 处理含特殊字符的键值(如?=
    • 支持无值参数(如&key
    • 处理参数中出现的等号和问号

查询参数解析方法对比

特性GetQueryDictionaryGetQueryCollection
返回值类型Dictionary<string, string>NameValueCollection
键唯一性✔️(后值覆盖前值)✖️(支持多值键)
读取不存在键抛出异常返回null
参数格式键=值(值中可含=)键=值(值中可含=)
无值参数处理键→null键→null

核心方法实现解析

1. 查询参数解析逻辑

// 解析字典示例
int indexQuestionMark = url.IndexOf('?');
if (indexQuestionMark > -1)
{
    string queryString = url.Substring(indexQuestionMark + 1);
    string[] pairs = queryString.Split('&');
    foreach (var pair in pairs)
    {
        int indexEquals = pair.IndexOf('=');
        if (indexEquals > -1)
        {
            string key = Uri.UnescapeDataString(pair.Substring(0, indexEquals));
            string value = Uri.UnescapeDataString(pair.Substring(indexEquals + 1));
            dict[key] = value; // 字典直接赋值(覆盖)
        }
        else
        {
            dict[pair] = null; // 无值参数处理
        }
    }
}
baseUrl = url.Substring(0, indexQuestionMark);

2. URL构建逻辑

// URL拼接核心逻辑
private static void GetUrlQueryStringCore(NameValueCollection parameters, StringBuilder builder)
{
    foreach (string key in parameters.Keys)
    {
        string[] values = parameters.GetValues(key);
        foreach (string value in values)
        {
            // 添加分隔符(?或&)
            if (builder.Length > baseUrlLength) builder.Append('&');
            else if (needQuestionMark) builder.Append('?');
            
            builder.Append(Uri.EscapeDataString(key));
            if (value != null)
            {
                builder.Append('=');
                builder.Append(Uri.EscapeDataString(value));
            }
        }
    }
}

特殊场景处理说明

  1. 值中含等号
    使用首次出现的=分割键值:

    // 示例参数:title=index=1
    // 解析结果:key="title", value="index=1"
    int indexEquals = pair.IndexOf('=');
    
  2. 无值参数
    被解析为key → null

    // 示例URL:https://site.com?key1&key2=val
    // 解析结果:key1=null, key2="val"
    
  3. 问号处理
    仅识别第一个?作为查询起始:

    // 示例:https://site.com?param=?
    // 正确解析:param="?"
    int indexQuestionMark = url.IndexOf('?');
    

测试用例验证

测试场景:
1. 标准URL解析
   ✓ https://site.com?k1=v1&k2=v2 → k1="v1", k2="v2"
   
2. 特殊字符处理
   ✓ https://site.com?key=? → key="?"
   ✓ https://site.com?k=1=2=3 → k="1=2=3"
   
3. 边界情况
   ✓ https://site.com? (空参数) → 空集合
   ✓ https://site.com (无参数) → 空集合
   
4. URL重建验证
   ✓ 解析后重建URL应与原始URL一致

使用注意事项

  1. 字典读取风险

    // 安全读取方式
    if (dict.TryGetValue("key", out var value)) { ... }
    
  2. 编码规范

    • 使用Uri.EscapeDataString处理特殊字符
    • 使用Uri.UnescapeDataString反向解码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值