ASP.NET MVC 请求流程:Route

2021-07-01 .NET高级班 86- ASP.NET MVC 管道模型详解 ASP.NET MVC管道模型图 HttpModule管道 public class BaseModule : IHttpModule { /// <summary> /// Init方法仅用于给期望的事件注册方法 /// </summary> /// <param name="httpApplication"></param> public void Init(HttpApplication httpApplica 阅读详情

1.RouteTable

RouteTable翻译过来的意思就是路由表,一个Web应用程序具有一个全局的路由表,该路由表通过System.Web.Routiing.RouteTable的静态只读属性Routes表示,该类型返回一个类型为System.Web.Routingg.RouteCollection的集合。

RouteTable类十分的简单,如下所示

public class RouteTable
{
    private static RouteCollection _instance = new RouteCollection();

    //返回一个静态只读的RouteCollection类型实例
    public static RouteCollection Routes
    {
        get
        {
            return RouteTable._instance;
        }
    }
    public RouteTable()
    {
    }
}

现在我们来看一下运行时的状态

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes); //断点1
    }
}

下图就是当断点处于断点1时,RouteTable的状态 ,我们在这里可以很清楚的看到现在Routes属性所包含的路由条数为0,不急我们继续向下走。

2.RouteCollection

看到名称就不难猜到,这个应该是表示路由集合,我们先来看看这个类里面有什么新奇玩意。

public class RouteCollection : Collection<RouteBase>
{
        //其余省略

        //是否添加首尾斜杠.默认值为 false.
        public bool AppendTrailingSlash { get; set; }
        //是否将 URL 转换为小写.默认值为 false.
        public bool LowercaseUrls { get; set; }
        //是否应处理与现有文件匹配的 URL.默认值为 false.
        public bool RouteExistingFiles { get; set; }
        //获取路由信息
        public RouteData GetRouteData(HttpContextBase httpContext);
        //获取虚拟路径信息
        public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values);
        //忽略路由URL和相关约束
        public void Ignore(string url, object constraints);
        //添加路由
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens);

}

稍微了解了这个类是用来干什么的,那么我们就要接着上面的程序向下走了,当然先介绍以下RouteBase和Route类吧

3.RouteBase,Route

在上图中我们看到了RouteBase,Route类,来说一下它们是什么吧。

RouteBase

RouteBase是Route类的父类,我们还是来看下它的类结构吧

public abstract class RouteBase
    {
        private bool _routeExistingFiles = true;

        // 指示 ASP.NET 路由操作是否应处理与物理文件匹配的 URL,这里默认是True,即可以使用WebForm方式请求物理文件,但是在MSDN中描述
        //这个属性的默认值为False
        public bool RouteExistingFiles
        {
            get
            {
                return this._routeExistingFiles;
            }
            set
            {
                this._routeExistingFiles = value;
            }
        }

        // 获取路由信息,保存在RouteData中
        public abstract RouteData GetRouteData(HttpContextBase httpContext);

        // 获取虚拟路径信息,保存在VirtualPathData中
        public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
    }

RouteURL模版模式的路由匹配规则就定义在Route中,看下类结构吧

public class Route : RouteBase
    {
        // 省略代码.....
        private string _url;
        private ParsedRoute _parsedRoute;
        private const string HttpMethodParameterName = "httpMethod";
        // 存储路由约束
        public RouteValueDictionary Constraints
        {
            get;
            set;
        }
        // 存储额外变量,但不会参与针对请求地址的匹配工作,比如Namespaces
        public RouteValueDictionary DataTokens
        {
            get;
            set;
        }
        // 存储为路由变量定义的默认值
        public RouteValueDictionary Defaults
        {
            get;
            set;
        }
        // 路由处理对象
        public IRouteHandler RouteHandler
        {
            get;
            set;
        }
        // URL模版
        public string Url
        {
            get
            {
                return this._url ?? string.Empty;
            }
            set
            {
                this._parsedRoute = RouteParser.Parse(value);
                this._url = value;
            }
        }
        public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
        {
            this.Url = url;
            this.Defaults = defaults;
            this.Constraints = constraints;
            this.DataTokens = dataTokens;
            this.RouteHandler = routeHandler;
        }
        // 重写父类方法
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
            RouteValueDictionary routeValueDictionary = this._parsedRoute.Match(virtualPath, this.Defaults);
            if (routeValueDictionary == null)
            {
                return null;
            }
            RouteData routeData = new RouteData(this, this.RouteHandler);
            if (!this.ProcessConstraints(httpContext, routeValueDictionary, RouteDirection.IncomingRequest))
            {
                return null;
            }
            foreach (KeyValuePair<string, object> current in routeValueDictionary)
            {
                routeData.Values.Add(current.Key, current.Value);
            }
            if (this.DataTokens != null)
            {
                foreach (KeyValuePair<string, object> current2 in this.DataTokens)
                {
                    routeData.DataTokens[current2.Key] = current2.Value;
                }
            }
            return routeData;
        }
        // 重写父类方法
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            BoundUrl boundUrl = this._parsedRoute.Bind(requestContext.RouteData.Values, values, this.Defaults, this.Constraints);
            if (boundUrl == null)
            {
                return null;
            }
            if (!this.ProcessConstraints(requestContext.HttpContext, boundUrl.Values, RouteDirection.UrlGeneration))
            {
                return null;
            }
            VirtualPathData virtualPathData = new VirtualPathData(this, boundUrl.Url);
            if (this.DataTokens != null)
            {
                foreach (KeyValuePair<string, object> current in this.DataTokens)
                {
                    virtualPathData.DataTokens[current.Key] = current.Value;
                }
            }
            return virtualPathData;
        }
        // 验证参数值是否与该参数的约束匹配
        protected virtual bool ProcessConstraint(HttpContextBase httpContext, object constraint, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            IRouteConstraint routeConstraint = constraint as IRouteConstraint;
            if (routeConstraint != null)
            {
                return routeConstraint.Match(httpContext, this, parameterName, values, routeDirection);
            }
            string text = constraint as string;
            if (text == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("Route_ValidationMustBeStringOrCustomConstraint"), new object[]
                {
                    parameterName,
                    this.Url
                }));
            }
            object value;
            values.TryGetValue(parameterName, out value);
            string input = Convert.ToString(value, CultureInfo.InvariantCulture);
            string pattern = "^(" + text + ")$";
            return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        }
    }

介绍完RouteBase和Route类后,我们的代码继续向下走

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        constraints: new { controller = "^H.*" },
        namespaces: new[] { "SimpleMVC" }
    );
}

看上面一段代码,我们发现RouteCollection实例有两个方法,但是System.Web.Routing.RouteCollection类中并没有这两个方法,那这个怎么实现的呢?

我们在IgnoreRoute上转到定义看下,发现我们跳转到了System.Web.Mvc.RouteCollectionExtensions这个路由集合扩展类了,在看下这个方法

public static void IgnoreRoute(this RouteCollection routes, string url)
{
    routes.IgnoreRoute(url, null);
}

一看恍然大悟,原来是通过扩展方法,感叹下扩展方法原来是可以这么用的。

好了,那么routes.MapRoute也肯定是通过扩展方法注入的。那我们就看下route.MaoRoute是实现的。

public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
    {
        if (routes == null)
        {
            throw new ArgumentNullException("routes");
        }
        if (url == null)
        {
            throw new ArgumentNullException("url");
        }
        // MvcRouteHandler 是请求进入时使用MVC路由关键
        Route route = new Route(url, new MvcRouteHandler())
        {
            // 存储为路由变量定义的默认值
            Defaults = RouteCollectionExtensions.CreateRouteValueDictionaryUncached(defaults),
            // 存储路由约束
            Constraints = RouteCollectionExtensions.CreateRouteValueDictionaryUncached(constraints),
            // 存储额外变量,但不会参与针对请求地址的匹配工作,比如Namespaces
            DataTokens = new RouteValueDictionary()
        };
        ConstraintValidation.Validate(route);
        if (namespaces != null && namespaces.Length > 0)
        {
           route.DataTokens["Namespaces"] = namespaces;
        }
        // 向RouteCollection中添加路由
        routes.Add(name, route);
        // 返回该路由
        return route;
    }

好了,我们大概已经了解这两个扩展方法的作用了,下面我们来看看它们在运行时的状态

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        constraints: new { controller = "^H.*" },
        namespaces: new[] { "SimpleMVC" }
    );// 断点2
}

我们看看当断点停留在断点2处时,类里面的状态是怎样的?如下图
在这里插入图片描述

我们可以很清楚的看到RouteCollection实例包含两条由规则,这两条路由规则都是继承自System.Web.Routing.RouteBase,第一条是我们定义为忽略的路由,类型是System.Web.Mvc.RouteCollectionExtensions.IgnoreRouteInternal,该类型继承子System.Web.Routing.Route,第二条则是我们定义的有效的路由,类型是System.Web.Routing.Route。

我们在深入看下第二条有效的路由信息
在这里插入图片描述
通过上图,可以非常明显的看出,哪些数据存储到了哪些属性里面,可以有个直观的理解。

4.关系

好了,我们的程序需要继续向下走,执行完RegisterRoutes方法后,我们又回到了Application_Start方法。

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes); // 断点1
} // 断点3

我们在断点3出看下各类的状态,总结下RouteTable,RouteCollection,RouteBase,Route4个类之间的关系,如下图所示
在这里插入图片描述

下面是一张RouteTable,RouteCollection,RouteBase,Route4个类关系图
在这里插入图片描述

详解ASP.NET MVC的运行 ASP.NET MVC的路由系统通过对HTTP请求的解析得到表示Controller、Action和其他相关的数据,并以此为依据激活Controller对象,调用相应的Action方法,并将方法返回的ActionResult写入HTTP回复中。为了更好的演示其实现原理,我创建一个简单的ASP.NET Web应用来模拟ASP.NET MVC的路由机制。这个例子中的相关组件基本上就是根据ASP.NET MVC的同名组件设计的,只是我将它们进行了最大限度的简化,因为我们只需要用它来演示大致的实现原理而已。[源代码 阅读详情

相关推荐

ASP.NET MVC:自定义 Route 让你的 Url 更优雅.

ASP.NET MVC:自定义 Route 让你的 Url 更优雅.ASP.NET MVC:自定义 Route 让你的 Url 更优雅.

ASP.NET Route

Route理解 路由,就字面意思到某个地方的指向地址,在程序中就指程序需要运行的类和方法。在Asp.net Webform中每个请求(URL)都指向某个具体的物理文件,这样限制了URL的灵活性、可读性和SEO优化。 Route的作用主要有: 根据URL提取RouteData,RouteData包含指向具体的执行方法的值(如ControllerName和ActionName)和处理的IH...

weixin_30325793的博客 866

channel || channelGroup || ctx.writeAndFlush()发送字符串消息客户端接收不到的原因

一、原因 网络上传播的数据都是字节(二进制数据)类型。我们如果直接向网络流里面写入一个对象(如:String对象),是无法传输的。只有将对象序列化(转换为可以传输的字节类型)之后,才可以进行传输。所以问题的根源就很清楚了,需要把字符串转换成字节进行传输。、 二、解决办法 1、手动序列化 服务端: channelGroup.writeAndFlush(Unpooled.copiedBuffer("【客户端】"+remoteAddress+"已下线"+format.format(new Date()), Cha

haohaoxuexiyai的博客 4059

(.NET进阶五)Route原理/约束/特殊url生成/Area分区

目录 一、理解路由的原理 二、路由规则定义与应用 三、路由数据、特殊匹配、默认值与约束 四、通过路由生成URL 五、分区项目 一、理解路由的原理 为什么要定义路由? ASP.NET-MVC项目开发中的地位 路由的定义->URL访问规则,占用的开发量很小,但是非常重要,因为任何请求都离不开路由 使用路由的好处 能够根据系统需求,灵活的划分请求规则(不...

Tiger_shl的博客 1589

Word标题样式基准重置:彻底解决重新打开后自动缩进问题

本文深入剖析了Word文档中标题样式在重新打开后自动缩进问题的根源,并提供了两种根治方案。通过调整标题样式基准或将其设为“无样式”,并配合检查修正多级列表设置,可彻底解决这一常见格式顽疾,确保文档格式的稳定性与专业性。

weixin_29266647的博客 526

SWD下载调试接口原理深度剖析

作为ARM嵌入式工程师,下载调试器都应该知道,但你真正了解SWD如何下载调试的么?ARM 芯片通过什么物理接口和什么方式连接完全取决于芯片Debug子系统的架构如何?

zhuwade的博客 1万+

asp.net core 属性路由和约定路由

属性路由()是一种在Web应用中通过应用于控制器和操作方法的特性(attribute)来定义路由的方式。与传统的基于路由表的路由配置(如在中配置路由)不同,属性路由允许在代码中直接声明路由路径,通常是通过在控制器或操作方法上使用特性来实现。在ASP.NETCore或ASP.NETMVC中,属性路由是一种非常常见的方式,它提供了更灵活和清晰的路由管理方式,尤其是在开发 RESTful API 时,它能够使得路由定义与控制器的行为紧密绑定。

情绪不稳定的98年程序员 1515

(超详细)Scala环境搭建(scala-2.12.15、IDEA2021.1.3)

Scala环境搭建(scala-2.12.15、IDEA2021.1.3) JunLeon——go big or go home 目录 Scala环境搭建(scala-2.12.15、IDEA2021.1.3) 一、Scala概述 1、什么是Scala? 2、Scala发展史 3、Scala与Java的关系 二、Scala环境搭建 1、环境准备 2、Scala安装与配置环境变量 3、验证Scala 三、编写第一个Scala程序 1、使用IDEA创建maven工程 2、.

JunLeon的博客 3万+

ASP.NET 路由设置 学习笔记

1. 创建表单,设置路径到 using(Html.BeginFrom( “方法”,“控制器”)) 创建文本输入框 Html.TextBoxFor(x=x.name) 路由配置,添加,程序启动时,先在Globe.asax 全局文件中读取 RouteConfig.RegisterRoutes(RouteTable.Routes); 调用APP_start 里面的RouteConf

Dragon1993vsLong的博客 1948

双参数威布尔分布(Weibull分布)参数计算(根据故障数据,求双参数威布尔分布的参数详细步骤)

根据故障数据求威布尔分布参数步骤 根据众多文献研究,设备故障数据大多服从指数分布、威布尔分布或对数正态分布。其中威布尔分布较为常用,本文主要介绍威布尔分布的简单应用,并根据某设备故障数据为例简单介绍威布尔分布参数的计算方法。 参考文献11参考文献22 1.双参数威布尔分布的概率密度函数、概率分布函数、可靠度函数: 2.预处理故障数据 将故障数据按时间排序,假设设备分别在以下时间点失效:11.1 、23.5、58.4……则将此时间从小到大排序。并计算中位秩。中位秩的计算公式如下: 其中i表示第几个数据,n

biaochengxiaobai的博客 4万+

IO Virtualization with Virtio.part 2 [十三]

通过对virtio_device_id 中的 device 和 vendor 的值进行比较,我们找到对应的 virtio 驱动,由于这里我们使用 i2c virtio 为例,所以调用 virtio_i2c_probe 进行 virtio i2c 设备的初始化。在和 virtio mmio 设备交互时我们会读取 VIRTIO_MMIO_DEVICE_ID 和 VIRTIO_MMIO_VENDOR_ID,并将其值写入 virtio_device_id 中,这个就会用作后面和 virtio 设备驱动的匹配参数。

cxjczy1990的博客 1211

ASP.NET 4的Demo实践:URL路由改进支持

ASP.NET 4的Demo实践:URL路由改进支持2010年07月10日 星期六 上午 08:41从.NET框架3.5 SP1开始,微软推出了ASP.NET路由支持,从而实现了特定资源的URL与其对应的Web服务器上的物理文件之间的彻底解耦。借助于ASP.NET路由支持,开发人员可以定义一组路由规则,从而实现把路由模式映射到一个生成相应内容的类。例如,你可以把URL“Categories/CategoryName”映射到一个类,该类接收CategoryName而最终生成对应于此种类的产品信息显示于一个网格

mayanly的专栏 642

AD22里面封装修改后,同步到PCB图里面

【摘要】本文介绍了Altium Designer 22中同步更新元件封装的三种方法:1)从PCB库直接更新:在PCB库面板右键选择"Update PCB With[封装名]";2)通过原理图标准更新:使用Design->Update PCB Document菜单;3)在PCB编辑器中验证更新:通过Tools->Update From PCB Libraries菜单。所有方法最终都需通过ECO(工程变更订单)确认变更,建议执行前仔细核对变更内容。这些方法适用于AD22及早期版本

whm128的博客 1757

ASP.NET MVC请求的服务过程

传统的URL往往对应了服务器磁盘物理路径的文件,而MVC的URL机制打破了这种束缚,使得服务器的文件组织可以不再暴露于众,而是映射到controller和action上。从图中可以初步看出一个HttpRequest是如何被ASP.NET和ASP.NET MVC框架执行的:经过IIS和ASP.NET处理后,Core Routing会首先根据URL匹配。会将这个请求“带入”MVC框架,执行Controller和Action,Action可以直接注入response,或者更平常的是返回一个。

支持一对一答疑的编程作家朱文伟的博客! 599

无人机——电机篇(一)

1. 电机的定义 电机俗称“马达”,是无人机的动力来源,无人机通过改变电机的转速来改变无人机的飞行状态。即改变每个电机的速度,使得无人机能够盘旋空中,上升或下降,或向各个方向移动。2. 电机的分类 电机分为:“无刷电机”和“有刷电机” - 无刷电机:是采用半导体开关器件来实现电子换向,具有可靠性高、低干扰、低噪声低、寿命长等优点。一般应用在稍大型的飞行器,载重大,可以有更广泛的用途。 - 有刷电机:是内含电刷装置的将电能转换成机械能(电动机)或将机械能转换成电能(发电机)的旋转电机。有刷电机是所有电机的基

weixin_46066938的博客 1万+

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

asp.net mvc源码分析-Route的GetRouteData

我知道Route这里东西应该算路由,这里把它放到mvc里面有些不怎么合适,但是我想大家多数遇到路由都是在mvc的时候吧.首先我们还是来看看GetRouteData方法吧 public override RouteData GetRouteData(HttpContextBase httpContext) { string virtualPath = httpContext.Reque

dz45693的专栏 4173

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

ASP.NET Core MVC之route简单路由配置

ASP.NET Core MVC的路由配置一般分为两种,约定路由和特性路由。下面分别介绍两种路由的配置方法。 一:约定路由 约定路由顾名思义,是我们约定好的路由规则,程序根据约定,访问对应的controller,首先建立一个ASP.NET Core Web项目,选择空模板,在Startup类中的ConfigureServices方法中注册MVC服务。 public void ConfigureSe...

weixin_39714231的博客 7454

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

ASP.NET MVC路由系统的核心对象介绍

众所周知,ASP.NET MVC有一套自己的路由系统。这套路由系统是在原来的ASP.NET 路由系统上扩展过来的。今天这篇文章就来聊聊MVC路由系统中非常关键的一些对象。 ASP.NET MVC路由系统主要由以下几个核心对象: 1.RouteCollection(RouteCollextionExtentions) 2.RouteTable 3.RouteData 4.Route:RouteBase 5.URLR

曹瑞鹏的博客(专注于.NET开发,Unity开发) 2291

ASP.NET MVC 在不同版本的 IIS 上使用 ASP.NET MVC

在不同版本的 IIS 上使用 ASP.NET MVCASP.NET MVC Framework 依赖于 URL 路由。为了利用 URL 路由,可能不得不在 Web 服务器上执行额外的配置步骤。这些步骤取决于 Internet Information Services (IIS) 的版本和应用程序的请求处理模式。IIS 的最新版本是版本 7.0。IIS 的此版本包括在 Windows Ser

deng_zz的专栏 2999

Asp.net MVC 自定义RouteHandler

<br />asp.net mvc 流程初探,可参考:<br /> <br />http://blog.csdn.net/shaily/archive/2009/07/02/4315547.aspx<br /> <br />    在ASP.NET MVC程序中首先涉及的部件是UrlRoutingModule,它是System.Web.Routing的一部分.UrlRoutingModule用于第一次检查请求的url和本地磁盘中的文件是否相匹配。如果匹配,UrlRoutingModule会将请求直接回发给I

sowhat430的专栏 3837

ASP.NET MVC 生命周期详解:从请求到视图渲染的完整流程

ASP.NET MVC 生命周期是理解框架行为与定制扩展的基础技术概念,其本质是ASP.NET经典管道(HttpApplication Pipeline)与MVC专属处理流(MvcHandler→Controller→ActionInvoker→ViewEngine)的双层嵌套结构。核心原理在于职责分离与时机控制:UrlRoutingModule在PostResolveRequestCache阶段解析RouteData,MvcHandler于PostMapRequestHandler接管请求,Control

didi9310的博客 392

.NET8入门:12.ASP.NET Core 中的路由

ASP.NET Core MVC是一种十分经典的软件设计模式架构,接下来我们来了解一下MVC在ASP.NET8中的一些基础概念。

全干摸鱼ing... 1182

ASP.NET Url请求过程

一、常规情况:不是MVC应用架构 1、对ASP.NET的请求根据Web应用程序首先穿过UrlRoutingModule对象,是HTTP Module。 2、再由UrlRoutingModule对象在RouteTable对象集合中按照已经定义好的路由规则去匹配Route对象(实际上是实现RouteBase对象), 如果路由不匹配, UrlRoutingModule对象什么都不做...

weixin_30932215的博客 447
上一篇: 在ASP.NET MVC中使用IIS级别的URL Rewrite
下一篇: ASP.NET Core端点路由 作用原理
.NET跨平台
博客等级 码龄15年 6066粉丝 738原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值