水果库存管理系统-进阶版2- disPatcherServlet版

6 篇文章 0 订阅
6 篇文章 0 订阅

水果库存管理系统-进阶版2- disPatcherServlet版

image-20220403123526715

做下面的操作之前,先设置一下这个,可以让接下来的反射可以获取类中方法的参数。

03.MVC03

如果说,当前的项目中不止有一个FruitServlet,而是还有UserServlet,ProductServlet等等不同的Servlet,且这些Servlet中的代码几乎都是相同的,我们就没必要写那么多个Servlet,我们可以只写一个Dispatcher Servlet,然后再根据请求判断该请求要发给原本的哪个Servlet(现在叫做Controller,哪个Controller)。

我们现在需要做的是什么呢?就是比如说//假设我们的url是: http://localhost:8080/pro15/Fruit.do,(或者说我们请求的Fruit.do)那么我们就能找到FruitController,如果我们请求(User.do),那么我们就能找到UserController。

而且我们可以用DispatcherServlet的统一处理,这样可以更好的配合反射(虽然我还没没学框架,但是已经能隐约感受出这大概是框架的灵魂)。

讲后面的案例就会大概有个思路。

首先是DispatcherServlet的service方法,也就是当DispatcherServlet中央过滤器收到请求时,它要做的第一件事是根据不同请求,让它对应到不同的Servlet。

思路是:
// 第1步: /hello.do ->   hello   或者  /fruit.do  -> fruit

String servletPath = request.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do") ;
servletPath = servletPath.substring(0,lastDotIndex);
// 第一步:hello.do ->   hello   或者  /fruit.do  -> fruit

这一步的实际作用就是将hello.do截取到只剩下hello

然后再下一步再根据hello去寻找与hello对应的Servlet,这些在那对应呢?

这就要讲到xml文件了:

xml文件

首先,我们讲一下xml文件。

1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集
2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
<beans>
    <!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
    <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
</beans>

xml文件是配置文件,里面是标签语言。

你可以写不同的标签,然后我们就可以在代码中取到这些标签中的内容,就比如上面的bean,就是我们直接定义的标签,而里面的id,class这些也是我们可以定义的。我们要先取到bean,再去找到id和class的对应,都是可以的。

比如我们上面写了一个id=“fruit” class=“com.atguigu.fruit.controllers.FruitController”/意思就是将它们对应在一起。待会讲下面的例子就懂了。

所以我们的第二部就是得通过 hello -> HelloController 或者 fruit -> FruitController。

不过在这之前,我们得先加载一下xml文件:

我们写在DispatcherServlet的init函数中:

   public void init() throws javax.servlet.ServletException {

        super.init();
        /*
        这个初始化就是解析xml文件,将其中对应的名称和类传进map中
         */
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点
            //可以理解这个document可以解析xml文件内的所有标签,所以只需要给出标签名!
            //就可以获取以该标签名的所有的行,获取后返回一个NodeList
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                //将每一行取出来
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                    Element beanElement = (Element)beanNode ;
                	// 取出每一个bean标签行后面的值,因为我们在里面写的是id=?class=?
                   //所以下面就根据id,class来获取。
                    String beanId =  beanElement.getAttribute("id");
                    String className = beanElement.getAttribute("class");
                    //获取到对应的类之后,就获取该类的实例,然后将名称和实例名传进map
                    Class controllerBeanClass = Class.forName(className);
                    //创建该类的实例
                    Object beanObj = controllerBeanClass.newInstance() ;
                    //将名称和实例传进map
                    beanMap.put(beanId , beanObj) ;
                    //比如说xml文件中写的是:
                      <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
                       那么取出来后就是map中创建一个映射
                          fruit 对应 FruitController的实例
                    
                    
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

就是我们要取得hello到helloServlet的对应还是需要做一系列操作的,这些操作就可以写在初始化函数init中,这些操作简而言之,就是新建了一个map,然后存入了hello和helloServlet的映射。

所以在这之后,service方法中就可以通过刚刚截取到的hello然后在init初始化函数时创建的map中取到相应对应的实例

//创建具体对应的实例对象
Object controllerBeanObj = beanMap.get(servletPath);
//进行不同请求时都会传一个方法名过来

那么获取到一个实例后,接下来要做什么呢?

接下来要做的事就重要了。

Dispatcher真正重要的事才刚刚开始,它真正的目的只有一个,就是反射!

其实我们是需要Dispatcher帮我们过滤吗?并不是,我们原本就可以在不同的Servlet中写注射,它们自身就可以被区分开来。我们真正要实现的事,任何的请求都到我DispatcherServlet中来,然后需要 FruitController干什么事,或者需要UserController干什么事,DispatcherServlet再调用它们的方法就行了。

那么如果有很多个类似FruitController的类,且其中的方法都不尽相同,难道我们要一个个实例化,然后一个个创建其中的函数吗?这样显然比原本还耦合复杂,如何将其变得更加简单,那么就用反射机制。

这就是反射的灵魂,而反射就是框架的灵魂。理解了这些,也就渐渐能懂得了框架。

我们要做的事,就是让Controller尽可能的简单,最好让其变为一个普通的类,有关于任何Http,Servlet的代码实现都交由DisPatcherServlet去完成,有没有可能?有,利用好反射机制,都有可能。

好,所以我们接下来的目标就是将将FruitController改为不含http的类。

首先,我们看原本的FruitController。

第一部分:

首先使原本的service方法

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //设置编码
    request.setCharacterEncoding("UTF-8");

    String operate = request.getParameter("operate");
    if(StringUtil.isEmpty(operate)){
        operate = "index" ;
    }
    //可以获取到该类的方法的数组
    Method[] methods=this.getClass().getDeclaredMethods();
    for(Method m:methods){
        String methodName=m.getName();
        if(operate.equals(methodName)){
            try {
                m.invoke(this,request,response);
                return;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    throw new RuntimeException("operate值非法")
}

这部分是可以很简单地可以搬到disPatcherServlet中去的,因为

  1. String operate = request.getParameter(“operate”);这里的参数是从request中获取的,我们也可以在disPatcherServlet直接获取就好。
  2. Method[] methods=this.getClass().getDeclaredMethods();这个methods是通过该FruitServlet的反射机制找到该方法数组的,而我们刚刚在

disPatcherServlet可以通过xml文件中获得FruitServlet的实例,所以也可以获得FruitServlet的方法数组,且有实例指针可以调用里面的方法。所以FruitServlet的service方法甚至可以不要了。

其二,看里面的函数,比如说edit,可否改成与http无关的。

private void edit(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
    String fidStr = request.getParameter("fid");
    if(StringUtil.isNotEmpty(fidStr)){
        int fid = Integer.parseInt(fidStr);
        Fruit fruit = fruitDAO.getFruitByFid(fid);
        request.setAttribute("fruit",fruit);
        super.processTemplate("edit",request,response);
    }
}

仔细观察一下,与http有关的由两部分,一是通过request来获取各种参数,这里我们想,能否从disPatcherServlet获取好参数,然后再传进来,可以的。

第二是 super.processTemplate(“edit”,request,response);这里与request和resonpse都有关,但是disPatcherServlet都有request和resonpse,能否让其在disPatcherServlet中处理?也是可以的。只需要传个参数过去。

所以我们可以将其改为这样:

private String edit(Integer fid , HttpServletRequest request){
    if(fid!=null){
        Fruit fruit = fruitDAO.getFruitByFid(fid);
        request.setAttribute("fruit",fruit);
        //super.processTemplate("edit",request,response);
        return "edit";
    }
    return "error" ;
}
private void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1.设置编码
    request.setCharacterEncoding("utf-8");

    //2.获取参数
    String fidStr = request.getParameter("fid");
    Integer fid = Integer.parseInt(fidStr);
    String fname = request.getParameter("fname");
    String priceStr = request.getParameter("price");
    int price = Integer.parseInt(priceStr);
    String fcountStr = request.getParameter("fcount");
    Integer fcount = Integer.parseInt(fcountStr);
    String remark = request.getParameter("remark");

    //3.执行更新
    fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));

    response.sendRedirect("fruit.do");
}
private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
    //3.执行更新
    fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
    //4.资源跳转
    return "redirect:fruit.do";
}

而看原本的update,它原本最后一句是 response.sendRedirect(“fruit.do”);

我们要区分一下原本 super.processTemplate(“edit”,request,response)和response.sendRedirect(“fruit.do”),所以传不同的字符串过去让disPatcherServlet处理。

但是Index简化到最后还是得有一个request参数,因为其在代码逻辑中需要session域保存值,所以无法再删减

private String index(String oper , String keyword , Integer pageNo , HttpServletRequest request ) {
    HttpSession session = request.getSession() ;

    if(pageNo==null){
        pageNo = 1;
    }
    if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
        pageNo = 1 ;
        if(StringUtil.isEmpty(keyword)){
            keyword = "" ;
        }
        session.setAttribute("keyword",keyword);
    }else{
        Object keywordObj = session.getAttribute("keyword");
        if(keywordObj!=null){
            keyword = (String)keywordObj ;
        }else{
            keyword = "" ;
        }
    }

    // 重新更新当前页的值
    session.setAttribute("pageNo",pageNo);

    FruitDAO fruitDAO = new FruitDAOImpl();
    List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
    session.setAttribute("fruitList",fruitList);

    //总记录条数
    int fruitCount = fruitDAO.getFruitCount(keyword);
    //总页数
    int pageCount = (fruitCount+5-1)/5 ;
    session.setAttribute("pageCount",pageCount);

    return "index" ;
}

然后现在主要看disPatcherServlet那边如何实现了。

一点一点讲吧。

String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
    operate = "index" ;
}

首先,和之前一样,获取传过来的方法名,判断是要实行什么方法。

然后刚刚上面不是通过

Object controllerBeanObj = beanMap.get(servletPath);

映射获得了要请求的control的实例。

所以就获得该实例的类的所有方法

Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();

(慢慢感受到xml文件搭配反射的强大之处了吧。不要写具体实现,简化了很多代码)

for(Method method : methods){
    if(operate.equals(method.getName())){
        //1.统一获取请求参数

        //1-1.获取当前方法的参数,返回参数数组
        Parameter[] parameters = method.getParameters();
        //1-2.parameterValues 用来承载参数的值
        Object[] parameterValues = new Object[parameters.length];

遍历这些方法,如果有一个方法和我们请求的方法名相同的话,

就获取该方法的参数名(反射有这个机制),然后返回一个数组。

private String add(String fname , Integer price , Integer fcount , String remark ) {
    Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
    fruitDAO.addFruit(fruit);
    return "redirect:fruit.do";
}

就比如刚刚的add函数,如上图,那么返回的数组的组成就是 fname,price,fcount,remark(参数名数组

Object[] parameterValues = new Object[parameters.length];

然后创建一个参数值数组,来存放参数数组中各参数的值。

for (int i = 0; i < parameters.length; i++) {
    Parameter parameter = parameters[i];
    String parameterName = parameter.getName() ;
    //如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
    if("request".equals(parameterName)){
        parameterValues[i] = request ;
    }else if("response".equals(parameterName)){
        parameterValues[i] = response ;
    }else if("session".equals(parameterName)){
        parameterValues[i] = request.getSession() ;

从参数名数组中一个个获得参数的名字,比如刚刚的add函数例子,就依次获得 fname,price,fcount,remark

因为刚说了比如index方法,有些参数可能是request,response,session,所以如果是request,response,session,那么在存放参数值的数组内就直接写入它们。

而如果不是的话,比如刚刚的add例子,参数:fname,price,fcount,remark

}else{
    //从请求中获取参数值
    String parameterValue = request.getParameter(parameterName);
    String typeName = parameter.getType().getName();

    Object parameterObj = parameterValue ;

    if(parameterObj!=null) {
        if ("java.lang.Integer".equals(typeName)) {
            parameterObj = Integer.parseInt(parameterValue);
        }
    }

    parameterValues[i] = parameterObj ;
}

那么比如说取到fname,就从request请求中去获取fname的值,然后存到参数值数组当中去

同时还要注意,从request获取值,获取的值都是String类型,所以如果判断到当前的参数的类型是int类型或其它类型的话

比如是int类型,那么得将该String转为Integer,然后再传入参数值数组中

那么我们要这个参数值数组有何用呢?

因为在反射机制中,我们要调用一个实例的方法,需要传实例和传参数进去。所以下面:

//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);

让它执行方法,而且我们刚刚fruitController不是还有一些跳转页面的操作需要在外面处理吗?

下面就是了:

  //3.视图处理
        String methodReturnStr = (String)returnObj ;
        if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
            String redirectStr = methodReturnStr.substring("redirect:".length());
            response.sendRedirect(redirectStr);
        }else{
            super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
        }
    }
}

所以总的disPatcher的文件就是这样了:

@WebServlet("*.do") //只要是什么.do都找到这里来
public class DispatcherServlet extends ViewBaseServlet{

    private Map<String,Object> beanMap = new HashMap<>();

    public DispatcherServlet(){
    }

    public void init() throws javax.servlet.ServletException {

        super.init();
        /*
        这个初始化就是解析xml文件,将其中对应的名称和类传进map中
         */
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点
            //可以理解这个document可以解析xml文件内的所有标签,所以只需要给出标签名!
            //获取一系列标签名为bean的行
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                //将每一行取出来
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                    Element beanElement = (Element)beanNode ;
                // 取出每一行id后面的值和class后面的值
                    String beanId =  beanElement.getAttribute("id");
                    String className = beanElement.getAttribute("class");
                    //返回对应的类
                    Class controllerBeanClass = Class.forName(className);
                    //创建该类的实例
                    Object beanObj = controllerBeanClass.newInstance() ;
                    //将名称和实例传进map
                    beanMap.put(beanId , beanObj) ;
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //设置编码
        request.setCharacterEncoding("UTF-8");
        //假设url是:  http://localhost:8080/pro15/hello.do
        //那么servletPath是:    /hello.do
        // 我的思路是:
        // 第1步: /hello.do ->   hello   或者  /fruit.do  -> fruit
        // 第2步: hello -> HelloController 或者 fruit -> FruitController
        String servletPath = request.getServletPath();
        servletPath = servletPath.substring(1);
        int lastDotIndex = servletPath.lastIndexOf(".do") ;
        servletPath = servletPath.substring(0,lastDotIndex);
        //创建具体对应的实例对象
        Object controllerBeanObj = beanMap.get(servletPath);
        //进行不同请求时都会传一个方法名过来
        String operate = request.getParameter("operate");
        if(StringUtil.isEmpty(operate)){
            operate = "index" ;
        }

        try {
            //获取一个类中的所有方法
            Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
            for(Method method : methods){
                if(operate.equals(method.getName())){
                    //1.统一获取请求参数

                    //1-1.获取当前方法的参数,返回参数数组
                    Parameter[] parameters = method.getParameters();
                    //1-2.parameterValues 用来承载参数的值
                    Object[] parameterValues = new Object[parameters.length];
                    for (int i = 0; i < parameters.length; i++) {
                        Parameter parameter = parameters[i];
                        String parameterName = parameter.getName() ;
                        //如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
                        if("request".equals(parameterName)){
                            parameterValues[i] = request ;
                        }else if("response".equals(parameterName)){
                            parameterValues[i] = response ;
                        }else if("session".equals(parameterName)){
                            parameterValues[i] = request.getSession() ;
                        }else{
                            //从请求中获取参数值
                            String parameterValue = request.getParameter(parameterName);
                            String typeName = parameter.getType().getName();

                            Object parameterObj = parameterValue ;

                            if(parameterObj!=null) {
                                if ("java.lang.Integer".equals(typeName)) {
                                    parameterObj = Integer.parseInt(parameterValue);
                                }
                            }

                            parameterValues[i] = parameterObj ;
                        }
                    }
                    //2.controller组件中的方法调用
                    method.setAccessible(true);
                    Object returnObj = method.invoke(controllerBeanObj,parameterValues);

                    //3.视图处理
                    String methodReturnStr = (String)returnObj ;
                    if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
                        String redirectStr = methodReturnStr.substring("redirect:".length());
                        response.sendRedirect(redirectStr);
                    }else{
                        super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
                    }
                }
            }

            /*
            }else{
                throw new RuntimeException("operate值非法!");
            }
            */
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

// 常见错误: IllegalArgumentException: argument type mismatch

@WebServlet(“*.do”) //注意最上面要给其一个注解,只要是什么.do都找到这里来。

然后FruitController的代码:

package com.atguigu.fruit.controllers;

import com.atguigu.fruit.dao.FruitDAO;
import com.atguigu.fruit.dao.impl.FruitDAOImpl;
import com.atguigu.fruit.pojo.Fruit;
import com.atguigu.myssm.util.StringUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

public class FruitController {
    private FruitDAO fruitDAO = new FruitDAOImpl();

    private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
        //3.执行更新
        fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
        //4.资源跳转
        return "redirect:fruit.do";
    }

    private String edit(Integer fid , HttpServletRequest request){
        if(fid!=null){
            Fruit fruit = fruitDAO.getFruitByFid(fid);
            request.setAttribute("fruit",fruit);
            //super.processTemplate("edit",request,response);
            return "edit";
        }
        return "error" ;
    }

    private String del(Integer fid  ){
        if(fid!=null){
            fruitDAO.delFruit(fid);
            return "redirect:fruit.do";
        }
        return "error";
    }

    private String add(String fname , Integer price , Integer fcount , String remark ) {
        Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
        fruitDAO.addFruit(fruit);
        return "redirect:fruit.do";
    }

    private String index(String oper , String keyword , Integer pageNo , HttpServletRequest request ) {
        HttpSession session = request.getSession() ;

        if(pageNo==null){
            pageNo = 1;
        }
        if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
            pageNo = 1 ;
            if(StringUtil.isEmpty(keyword)){
                keyword = "" ;
            }
            session.setAttribute("keyword",keyword);
        }else{
            Object keywordObj = session.getAttribute("keyword");
            if(keywordObj!=null){
                keyword = (String)keywordObj ;
            }else{
                keyword = "" ;
            }
        }

        // 重新更新当前页的值
        session.setAttribute("pageNo",pageNo);

        FruitDAO fruitDAO = new FruitDAOImpl();
        List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
        session.setAttribute("fruitList",fruitList);

        //总记录条数
        int fruitCount = fruitDAO.getFruitCount(keyword);
        //总页数
        int pageCount = (fruitCount+5-1)/5 ;
        session.setAttribute("pageCount",pageCount);

        return "index" ;
    }
}

总结一下:

  1. 首先,在xml文件中写下不同的名称和controller的对应

  2. 无论什么请求,都找到disPatcherServlet(disPatcher的初始化函数中将xml文件中的名称和对应的实例写在map中)

  3. 当请求到来时,dispatcher先是截取其来源,找到其想要请求的Controller。然后找到该controller的实例。

  4. 通过该实例获得该类的方法数组,然后从请求中获取请求的方法,将方法与方法数组一一对应,知道找到要执行的方法。

  5. 通过反射查询该方法的参数名,然后通过这些参数名到请求中获取参数,保存下来,然后再通过反射,将实例和这些参数的值一起传进去执行。

  6. 所以就是实现了,先找到对应的controller,再找到对应的方法然后执行的过程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值