MVC源码学习:打造自己的MVC框架(一:核心原理)

 

正文

前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎。这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之处,还希望大家斧正,博主感激不尽!

本文原创地址:http://www.cnblogs.com/landeanfen/p/5989092.html

MVC源码学习系列文章目录:

一、MVC原理解析

 最近园子里Asp.Net Core火了一阵,不管微软的开源动作有多么迟缓,还是希望微软能够给力一次。作为Core的主要Web框架——MVC,虽然已经开源,但是读起来着实费劲,并且感觉很多核心部件都找不到。于是只能通过Reflector去反编译MVC5的组件以及参考博客园Fish Li等大神的文章去学习下MVC5的原理。

10月26日更新:感谢园友Adming在评论中提醒,原来Asp.net Core Mvc和Asp.net Mvc 5的原理已经完全不同,难怪在Core Mvc的源码里面已经找不到MvcHandler、UrlRoutingModule等核心部件了呢,此系列文章就先学习下MVC5的原理,等以后有空了再来研究Core MVC吧。

Asp.Net Core MVC的开源地址:https://github.com/aspnet/Mvc

Asp.net MVC的开源地址:http://aspnetwebstack.codeplex.com/SourceControl/latest

1、MVC原理

之前的文章有介绍MVC的路由机制,其实路由机制算是MVC的原理的核心之一。在此我们还是要不厌其烦再来谈谈整个过程,因为这是理解MVC原理不可逾越的鸿沟。当我们收到一个URL的请求时,服务端收到请求,主要经历以下几个步骤:

  1. 请求被UrlRoutingModule部件拦截
  2. 封装请求上下文HttpContext,成为HttpContextWrapper对象。
  3. 根据当前的HttpContext,从Routes集合中得到与当前请求URL相符合的RouteData对象。
  4. RouteDataHttpContext请求封装成一个RequestContext对象。
  5. 根据RequestContext对象,从RouteData的RouteHandler中获取IHttpHandler(MVC里面会有一个IHttpHandler的实现类MvcHandler)。
  6. 执行IHttpHandler(MvcHandler),然后就是通过反射激活具体的controller,执行具体的action。

附上一张大致的流程图:

纵观整个过程,看上去很复杂,各种对象缠绕,看得人晕晕的。其实如果你静下心来仔细研读MVC的源码你会发现其实并没有想像中的那般复杂,请有点耐心听博主慢慢道来。

1、整个过程有两个核心的组件,文中博主用红色标记了出来:UrlRoutingModuleMvcHandler,上文提到的各个过程都和两个组件有紧密的联系。而这两个组件分别继承至IHttpModule和IHttpHandler接口,熟悉Asp.net管线事件的朋友应该会记得这两个接口,在管道事件里面这两个接口扮演着重要角色。要理解MVC的上述原理,必须要先理解这两类接口的原理以及使用。

2、UrlRoutingModule的作用可以理解为通过一系列的与路由相关的组件去解析当前请求的Controller与Action名称,其实简单点理解,比如我们请求http://localhost:8080/Home/Index这个url的时候,UrlRoutingModule拦截到这个请求,然后通过一系列的方式得到这里的“Home”和“Index”,这样理解有没有简单一点呢。

3、MvcHandler的作用就更加直接,上述通过拦截组件得到了请求的Controller和Action的名称,MvcHandler组件将当前请求的Controller名称反射得到对应的控制器对象,然后执行对应的Action方法。比如还是上述http://localhost:8080/Home/Index这个请求,通过字符串“Home”反射成为Home这个类型的控制器对象,然后调用这个对象的Index()方法。

4、综上,联合这两个组件来理解,UrlRoutingMudule组件的主要作用是解析当前的Controller与Action名称,MvcHandler的作用是将得到的Controller名称激活,得到具体的Controller对象,然后执行对应的Action方法。

所以,要理解MVC的原理,必须要了解这两个组件的基本原理以及作用。下面就根据这两个组件分别展开说明,相信理解了下面的内容,你对mvc的原理会有一个新的认识。

二、HttpHandler

上文说过MvcHandler是继承至IHttpHandler接口的!为什么这里大标题会用HttpHandler而不是MvcHandler呢?因为博主觉得,HttpHandler实在是太重要了,首先得理解了HttpHandler这么一个大的东西,然后再来看具体的MvcHandler才有意义。

1、HttpHandler、IHttpHandler、MvcHandler的说明

  • HttpHandler指所有实现IHttpHandler接口一类类型的统称,它是一个大的称谓。这些类型有一个共同的功能,那就是可以用来处理Http请求。
  • IHttpHandler是微软定义的一类接口,用来约束所有能够处理Http请求的类型的接口规则。
  • MvcHandler是Mvc里面实现IHttpHandler接口的类型,也就是说,MvcHandler是Mvc里面处理Http请求的类型。

总而言之,HttpHandler只是一个逻辑称谓,它并不具体存在。而IHttpHandler和MvcHandler是.net framework里面具体存在的接口和实现类,是前者的表现形式。

2、IHttpHandler解析

 2.1、Asp.net管线事件简易说明

做过Webform开发的园友应该记得,在asp.net的页面生命周期里面,一共有24个管线事件,完整的管线事件可参考MSDN文档:

  Asp.net管线事件说明

这里不可能把每个管线事件将清楚,但是在整个管线事件中,有两个重要的角色就是HttpHandlerHttpModule。在这些事件中,第10个事件【根据所请求资源的文件扩展名(在应用程序的配置文件中映射),选择实现 IHttpHandler 的类,对请求进行处理】 是HttpHandler创建的地方。关于WebForm里面HttpHandler创建的详细过程,这里就不展开说了,如果有兴趣可以参考http://www.cnblogs.com/fish-li/archive/2012/01/29/2331477.html

2.2、Asp.net中常见的HttpHandler类型

首先还是来看看IHttpHandler的定义

复制代码
public interface IHttpHandler
{
    // 定义一个处理当前http请求的方法
    void ProcessRequest(HttpContext context);

    // 指示当前实例是否可以再次使用
    bool IsReusable { get; }
}

 
复制代码

接口的定义很简单,ProcessRequest()方法里面传一个当前请求的上下文对象去处理当前的http请求。

为了处理异步请求,Framework里面还定义了一个异步的IHttpHandler接口:

public interface IHttpAsyncHandler : IHttpHandler
{
    // Methods
    IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    void EndProcessRequest(IAsyncResult result);
}

接口的两个方法应该也不难理解。

我们已经说了,HttpHandler的主要作用是处理http请求,原来在做webform的时候应该都写过后缀ashx的一般处理程序吧,这个一般处理程序就是通过实现IHttpHandler接口去实现的。我们是否曾经也写过类似这样的代码,新建一个TestHttpHandler.ashx文件,代码如下:

复制代码
public class TestHttpHandler : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "text/plain";

            var username = context.Request.QueryString["username"];
            var password = context.Request.QueryString["password"];
            if (username == "admin" && password == "admin")
            {
                context.Response.Write("用户admin登录成功");
            }
            else
            {
                context.Response.Write("用户名或者密码错误");
            }
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
复制代码

然后运行,通过http://localhost:16792/TestHttpHandler.ashx?username=admin&password=admin去访问一般处理程序,即可得到正确的结果。

当然,除了这个,还有我们最常见的aspx页面。

复制代码
    public partial class TestPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
    }
复制代码

将Page类转到定义:

发现原来Page类也是继承至IHttpHandler,这就是为什么我们可以通过地址http://localhost:16792/TestPage.aspx来访问这个页面的原因。当然,子类中的ProcessRequest()方法并没有显示的声明出来,因为在Page类里面已经有一个virtue的虚方法,如果需要,你也可以在TestPage这个类里面显示声明:

复制代码
    public partial class TestPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public void ProcessRequest(HttpContext context)
        {
            context.Response.Write("你好");
        }
    }
复制代码

然后你会发现这个时候请求会进到ProcessRequest()方法,而不会进到Page_Load()里面了,至于原因,这和Page类里面的封装有关系。当然这不是本文的重点,本文要说明的是所有实现了IHttpHandler接口的类型都可以在ProcessRequest()方法里面处理当前http请求。

当然,除了ashx和aspx以外,还有一类http的服务接口处理文件asmx也和IHttpHandler有着不可分割的联系,可以说,在asp.net里面,只要是处理Http请求的地方,IHttpHandler几乎“无处不在”。

2.3、自定义HttpHandler。

当然,除了上述asp.net自带的HttpHandler之外,我们也可以自定义HttpHandler处理特定的请求。比如我们新建一个TestMyHandler.cs页面:

复制代码
    public class TestMyHandler:IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            context.Response.Write("从asex页面进来");

            //throw new NotImplementedException();
        }
    }
复制代码

当然,要使用这个自定义的Handler需要在web.config里面加上配置。(PS:这部分是博主后来加上的,所以直接用正确的配置)

<system.webServer>
   <handlers>
        <add name="asex" verb="*" path="*.asex" type="MyTestMVC.TestMyHandler, MyTestMVC" preCondition="integratedMode" />
    </handlers>
</system.webServer>

这个配置的意思是所有的url以asex结尾的请求都交给TestMyHandler这个类去处理。得到效果:

3、MvcHandler解析

上文介绍了那么多IHttpHandler的用法,都是在WebForm里面的一些实现,我们知道了所有实现了IHttpHandler的类都可以处理Http请求。同样在MVC里面,也定义了一个实现IHttpHandler接口的类型——MvcHandler,用于处理当前的http请求。通过反编译工具可以看到:

复制代码
public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState
{
    // 省略若干字段// 所有方法
    static MvcHandler();
    public MvcHandler(RequestContext requestContext);
    protected internal virtual void AddVersionHeader(HttpContextBase httpContext);
    protected virtual IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state);
    protected internal virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state);
    protected internal virtual void EndProcessRequest(IAsyncResult asyncResult);
    private static string GetMvcVersionString();
    protected virtual void ProcessRequest(HttpContext httpContext);
    protected internal virtual void ProcessRequest(HttpContextBase httpContext);
    private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory);
    private void RemoveOptionalRoutingParameters();
    IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result);
    void IHttpHandler.ProcessRequest(HttpContext httpContext);

    // 省略若干属性
}
复制代码

MvcHandler实现了IHttpHandler、 IHttpAsyncHandler两个接口,异步请求这里先不做介绍。重点还是来看看ProcessRequest()方法

将HttpContext转换为HttpContextBase对象,继续转到定义。

这里声明了一个IController和IControllerFactory对象,通过this.ProcessRequestInit()方法创建具体的Controller实例。我们将ProcessRequestInit()方法转到定义

我们将代码复制出来,写入相应的注释:

复制代码
     private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
        {
            //1.得到当前的上下文
            HttpContext current = HttpContext.Current;
            if (current != null && ValidationUtility.IsValidationEnabled(current) == true) ValidationUtility.EnableDynamicValidation(current);
            this.AddVersionHeader(httpContext);
            this.RemoveOptionalRoutingParameters();

            //2.从路由对象RouteData中获取当前请求的Controller名称
            string requiredString = this.RequestContext.RouteData.GetRequiredString("controller");

            //3.得到Controller工厂对象
            factory = this.ControllerBuilder.GetControllerFactory();

            //4.根据当前RequestContext对象,从Controller工厂创建具体的Controller对象
            controller = factory.CreateController(this.RequestContext, requiredString);
            if (controller == null) throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MvcResources.ControllerBuilder_FactoryReturnedNull, new object[] { factory.GetType(), requiredString }));
        }
复制代码

通过上文的注释很好理解整个控制器的实例化过程。本打算看下Controller工厂如何创建以及控制器如何实例化的,奈何这部分反编译不了。我们暂且理解为反射吧,这些实现细节并不影响我们理解整个过程。

创建控制器成功之后,就是执行Action方法了,这个过程在上面反编译的第二张图片的 controller.Execute(this.RequestContext); 方法得到体现。所以,除去细节,理解MvcHandler的ProcessRequest()方法并不是太难。

三、HttpModule

除了HttpHandler之外,Asp.net里面还有另外一个重要的角色——HttpModule。和HttpHandler类似,HttpModule指所有实现了IHttpModule接口的一类类型的统称。至于HttpModule、IHttpModule、UrlRoutingModule各个名称的含义和上述HttpHandler相同,在此不做重复说明。

1、HttpModule能干什么

通过上文,我们知道HttpHandler的作用非常明确:处理Http请求,生成相应结果。那么,HttpModule又是干什么的呢?

HttpHandler的作用是处理某一类别的请求,比如ashx、aspx、asmx等,在某些情况下,各类请求可能都需要进行某些相同的处理(比如请求拦截、身份认证、检查功能等),不可能在每个类别的HttpHandler里面都去实现这些相同的代码,这个时候怎么办呢?处理某一类通用请求,提高代码的复用率。是不是想到了我们的面向切面编程(AOP),没错,HttpModule就是负责做这个事,HttpModule通过事件订阅的方式,将某类HttpHandler都需要的功能抽取出来,这些功能可以编译成类库供各个模块调用。这种采用事件(观察者)的设计模式使得系统设计上更加灵活。

2、HttpModule的使用

先来看看IHttpModule的定义

复制代码
public interface IHttpModule
{
    //初始化
    void Init(HttpApplication context);
   
    //释放
    void Dispose();
}
复制代码

接口定义很简单,一个初始化组件的方法,一个释放对象的方法。

我们来写一个测试的例子具体看看HttpModule如何注册事件,我们新建一个IHttpModule的实现类:

复制代码
namespace MyTestMVC
{
    public class TestMyModule:IHttpModule
    {
        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public void Init(HttpApplication app)
        {
            //事件注册
            app.BeginRequest += app_BeginRequest;
            app.EndRequest += app_EndRequest;
        }

        void app_EndRequest(object sender, EventArgs e)
        {
            var app = (HttpApplication)sender;
            app.Context.Response.Write("请求结束");
        }

        void app_BeginRequest(object sender, EventArgs e)
        {
            var app = (HttpApplication)sender;
            app.Context.Response.Write("请求开始");
        }
    }
}
复制代码

在Init方法里面,通过HttpApplication对象来注册请求的事件。这样,每次发起一次http请求的时候都进到这两个方法。

当然,这些注册就能执行了吗?想得美,系统哪里知道你这个自定义HttpModule的存在,所以必须要在Web.config里面声明一下。

 <system.web>
    <httpModules>
        <add name="TestMyModule" type="MyTestMVC.TestMyModule, MyTestMVC" />
    </httpModules>
  </system.web>

出现结果:

查阅资料后发现,原来IIS经典模式下必须要这样配置:

<system.webServer>
    <modules>
        <add name="TestMyModule" type="MyTestMVC.TestMyModule, MyTestMVC" preCondition="integratedMode" />
    </modules>
</system.webServer>

没办法,用微软的东西就要遵守别人的游戏规则。改成这样之后得到结果:

文中的“你好”来自这里:

 既然HttpModule是事件注册机制的,那么如果需要在同一个事件里面去实现不同的功能,也就是说同一个事件注册多次是否可行呢?我们来试一把:

假如TestMyModule.cs这个自定义Module的作用是功能检查:

  TestMyModule.cs

然后新建一个TestMyModule2.cs这个自定义Module,去实现请求拦截的功能:

  TestMyModule2.cs

最后在Web.config里面配置两个Module:

<system.webServer>
    <modules>
        <add name="TestMyModule" type="MyTestMVC.TestMyModule, MyTestMVC" preCondition="integratedMode" />
        <add name="TestMyModule2" type="MyTestMVC.TestMyModule2, MyTestMVC" preCondition="integratedMode" />
    </modules>
</system.webServer>

得到结果:

 

这说明同一个事件可以注册多次,即可以在同一个事件里面做不同的事。

3、HttpModule和HttpHandler如何区分

通过上文的HttpModule的应用,我们看到在Init方法里面可以拿到当前应用的HttpApplication对象,拿到这个貌似就可以拿到当前请求上下文里面的Request、Response了,是不是就可以处理当前的http请求了,从这点上来说,HttpModule也能处理http请求,或者说具有处理http请求的能力。既然HttpHandler和HttpModule都可以处理http请求,那在使用的时候如何区分呢?上文说过,HttpModule的作用类似AOP,是针对某些通用功能(请求拦截、身份认证、检查功能)的,而HttpHandler常用来处理某一类(ashx、aspx、asmx)http请求,两者的侧重点不同,至于具体在实际中如何使用,你可以自行考量。

4、UrlRoutingModule解析

好了,上面介绍那么多HttpModule的使用,都是在为了解Mvc里面的UrlRoutingModule做铺垫。上文说过UrlRoutingModule的作用是拦截请求,那么它是如何做的呢,还是来反编译看看吧。

复制代码
public class UrlRoutingModule : IHttpModule
{
    // Fields
    private static readonly object _contextKey;
    private static readonly object _requestDataKey;
    private RouteCollection _routeCollection;

    // Methods
    static UrlRoutingModule();
    public UrlRoutingModule();
    protected virtual void Dispose();
    protected virtual void Init(HttpApplication application);
    private void OnApplicationPostResolveRequestCache(object sender, EventArgs e);
    [Obsolete("This method is obsolete. Override the Init method to use the PostMapRequestHandler event.")]
    public virtual void PostMapRequestHandler(HttpContextBase context);
    public virtual void PostResolveRequestCache(HttpContextBase context);
    void IHttpModule.Dispose();
    void IHttpModule.Init(HttpApplication application);

    // Properties
    public RouteCollection RouteCollection { get; set; }
}
复制代码

重点肯定在Init()方法。

图一:

注册HttpApplication对象的PostResolveRequestCache事件。

图二:

封装HttpContext,成为HttpContextWrapper对象

图三:

这部分代码是我们上述路由理论的代码实践,所以这段代码很重要,我们将代码拷贝出来:

复制代码
     public virtual void PostResolveRequestCache(HttpContextBase context)
        {
            //1.传入当前上下文对象,得到与当前请求匹配的RouteData对象
            RouteData routeData = this.RouteCollection.GetRouteData(context);
            if (routeData != null)
            {
                //2.从RouteData对象里面得到当前的RouteHandler对象。其实这里的RouteHandler属性对应就是一个MvcRouteHandler的对象。
                IRouteHandler routeHandler = routeData.RouteHandler;
                if (routeHandler == null) throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.GetString("UrlRoutingModule_NoRouteHandler"), new object[0]));
                if (!(routeHandler is StopRoutingHandler))
                {
                    //3.根据HttpContext和RouteData得到RequestContext对象
                    RequestContext requestContext = new RequestContext(context, routeData);
                    context.Request.RequestContext = requestContext;

                    //4.根据RequestContext对象得到处理当前请求的HttpHandler(MvcHandler)。
                    IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
                    if (httpHandler == null)
                    {
                        object[] args = new object[] { routeHandler.GetType() };
                        throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("UrlRoutingModule_NoHttpHandler"), args));
                    }
                    if (httpHandler is UrlAuthFailureHandler)
                    {
                        if (!FormsAuthenticationModule.FormsAuthRequired) throw new HttpException(0x191, SR.GetString("Assess_Denied_Description3"));
                        UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this);
                    }
                    else
                        //5.请求转到HttpHandler进行处理(进入到ProcessRequest方法)。这一步很重要,由这一步开始,请求才由UrlRoutingModule转到了MvcHandler里面
                        context.RemapHandler(httpHandler);
                }
            }
        }
复制代码

博主在主要的地方加上了注释。

代码释疑:这里有几点需要说明的。

1、HttpApplication对象的PostResolveRequestCache事件在MSDN上的解释是:在 ASP.NET 跳过当前事件处理程序的执行并允许缓存模块满足来自缓存的请求时发生。查阅相关资料发现,之所以在PostResolveRequestCache事件注册路由、匹配HttpHandler,是为了满足IIS6。可以参考Tom大叔的文章:http://www.cnblogs.com/TomXu/p/3756858.html

2、 IRouteHandler routeHandler = routeData.RouteHandler; 这里的routeHandler实际上是一个MvcRouteHandler类型的对象,为什么这么说,我们来反编译下这个就会一目了然:

图一:

 

MvcRouteHandler实现了IRouteHandler接口。然后我们重点来看GetHttpHandler()方法得到的是哪个HttpHandler。

图二:

 

看到最后一句是不是立马就明白了。也就是说GetHttpHandler()这个方法决定了采用MvcHandler去处理当前的http请求。所以在上述 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 这一句得到的就是一个MvcHandler的实例。

3、 context.RemapHandler(httpHandler); 这一句可以理解为将当前的请求上下文交给httpHandler这个对象去处理。

4、到这里,我们再反过来看前面的MVC的原理就完全明朗了。

  1. 请求被UrlRoutingModule部件拦截————通过注册HttpApplication对象的PostResolveRequestCache事件来实现拦截
  2. 封装请求上下文HttpContext,成为HttpContextWrapper对象。————将UrlRoutingModule的Init()方法转到定义,可以看到这么一句: HttpContextBase context = new HttpContextWrapper(((HttpApplication) sender).Context); 
  3. 根据当前的HttpContext,从Routes集合中得到与当前请求URL相符合的RouteData对象。————将UrlRoutingModule的Init()方法转到定义,最终会找到PostResolveRequestCache()方法,方法里面有一句 RouteData routeData = this.RouteCollection.GetRouteData(context); 
  4. RouteDataHttpContext请求封装成一个RequestContext对象。————同样在上述方法里面 RequestContext requestContext = new RequestContext(context, routeData); 
  5. 根据RequestContext对象,从RouteData的RouteHandler中获取IHttpHandler(MVC里面会有一个IHttpHandler的实现类MvcHandler)。————同样在该方法里面 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 
  6. 执行IHttpHandler(MvcHandler)。———— context.RemapHandler(httpHandler); 将请求交给MvcHandler处理。
  7. 然后就是通过反射激活具体的controller,执行具体的action。————在MvcHandler的ProcessRequest()方法里面的执行逻辑

 四、总结

写到这里,总算把整个过程梳理了一遍,很多细节都未涉及,但是大的过程应该还是明朗的。通篇比较偏理论,所以整体上比较枯燥,但是还是希望园友们能够静下心来慢慢看,因为博主觉得这些对于理解MVC原理太重要!!!想想看,如果你也完全理解了这个过程,是不是都可以自己通过实现IHttphandler和IHttpModule去搭建一个简单的MVC框架了,不错,博主确实是这样打算的,这篇把理论搞清楚,下篇就是实现的细节了。其实写自己的MVC框架更多的在于学习MVC原理,希望自己能够坚持下去。如果你觉得本文能够帮助你,可以右边随意 打赏 博主,也可以 推荐 进行精神鼓励。你的支持是博主继续坚持的不懈动力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值