手写Spring-MVC之DispatcherServlet无组件版

Day47

手写Spring-MVC之DispatcherServlet

DispatcherServlet的思路:

前端传来URI,在TypeContainer容器类中通过uri得到对应的类描述类对象(注意:在监听器封装类描述类对象的时候,是针对于每一个URI进行封装的,也就是说每一个方法(uri)都有一个类描述类),通过类描述类对象获得类对象和方法描述类对象,再通过方法描述类对象获得方法对象和参数描述类对象。对参数描述类对象进行处理获得参数数组,然后通过method.invoke()执行方法,并返回值,最后处理返回值。

public class DispatherServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charSet=UTF-8");

        String uri = request.getRequestURI();
        HashMap<String, BeanDefinition> maps = TypeContainer.getMaps();
        BeanDefinition beanDefinition = maps.get(uri);
        if(beanDefinition==null){
            //System.out.println("没找到该uri下的类描述类");
            throw new FrameWorkException(ResponseCode.REQUEST_PATH_EXCEPTION.getCode(),ResponseCode.REQUEST_PATH_EXCEPTION.getMessage());
        }
        Object t = beanDefinition.getT();
        MethodDefinition methodDefinition = beanDefinition.getMethodDefinition();
        Method method = methodDefinition.getMethod();
        method.setAccessible(true);
        Model model = new Model();
        List<ParameterDefinition> parameterDefinitions = methodDefinition.getParameterDefinitions();
        Object[] args = handlerParameterArgs(parameterDefinitions, request, response,model);
        try{
            //调用Controller层里的某个方法
            Object returnVal = method.invoke(t, args);

            if(returnVal!=null){
                //处理返回值
                handlerReturnVal(methodDefinition,returnVal,request,response,model);
            }
        }catch (Exception e){
            System.out.println("出现异常了,处理全局异常");
        }


    }
    public void handlerReturnVal(MethodDefinition methodDefinition,Object returnVal,HttpServletRequest request,HttpServletResponse response,Model model) throws ServletException, IOException {

        if(methodDefinition.isResponseBodyHasOrNot()){
            //返回数据为JSON格式
            String jsonString = JSON.toJSONString(returnVal);
            sendResponse(response,jsonString);

        }
        else if(returnVal.getClass()==String.class){
            //如果返回值是字符串,先添加请求数据,再跳转(转发)

            handlerRequestVal(request,model.getMaps());
            jumpPage(returnVal,request,response);
        } else if (returnVal.getClass() == ModelAndView.class) {
            //如果返回值是ModelAndView,添加请求后跳转(转发)
            ModelAndView modelAndView = (ModelAndView) returnVal;
            handlerRequestVal(request,modelAndView.getMaps());
            jumpPage(modelAndView.getViewName(),request,response);

        }
    }
    public void sendResponse(HttpServletResponse response,String jsonString) throws IOException {
        response.getWriter().write(jsonString);
    }
    public void handlerRequestVal(HttpServletRequest request,Map<String,Object> maps){
        Set<Map.Entry<String, Object>> entries = maps.entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            String key = entry.getKey();
            Object value = entry.getValue();
            request.setAttribute(key,value);


        }
    }

    public void jumpPage(Object returnVal,HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
        String str = (String) returnVal;
        request.getRequestDispatcher(str).forward(request,response);
    }
    public Object[] handlerParameterArgs(List<ParameterDefinition> parameterDefinitions,HttpServletRequest request,HttpServletResponse response,Model model){
        if(parameterDefinitions==null){
            return null;
        }
        Object[] args = new Object[parameterDefinitions.size()];
        for (ParameterDefinition parameterDefinition : parameterDefinitions) {

            Class<?> clazz = parameterDefinition.getClazz();//获取参数的class对象
            String name = parameterDefinition.getName();//获取参数的名字
            int index = parameterDefinition.getIndex();//获取参数的下标
            if(judgeTypeIsJavaOrNot(clazz)){//判断是否是Java常用数据类型
                args = handlerJavaType(request, name, clazz, args, index);
            } else if (clazz==HttpServletRequest.class) {//判断是否是请求类型
                args[index] = request;
            } else if (clazz==HttpServletResponse.class) {//判断是否是响应类型
                args[index] = response;
            } else if (clazz == String[].class) {//判断是否是数组类型
                String[] arr = request.getParameterValues(name);
                args[index] = arr;
            } else if (clazz == List.class) {//判断是否是List集合类型
                handlerListType(request,parameterDefinition,args,index);

            } else if (clazz== Model.class) {//判断是否是Model类型
                args[index] = model;
            }else{//判断是否是自定义类型
                handlerOtherType(parameterDefinition,request,args,index);
            }
        }
        return args;
    }

    public void handlerOtherType(ParameterDefinition parameterDefinition,HttpServletRequest request,Object[] args,int index){
        try {
            if(parameterDefinition.isRequestBodyHasOrNot()){
                //如果返回的是JSON格式数据
                BufferedReader br = request.getReader();
                StringBuffer sb = new StringBuffer();
                char[] cs = new char[1024];
                int len;
                while((len= br.read(cs))!=-1){
                    sb.append(cs,0,len);
                }
                String jsonStr = sb.toString();
                Object obj = JSON.parseObject(jsonStr,parameterDefinition.getClazz());
                args[index] = obj;


            }else{
                Object obj = parameterDefinition.getClazz().newInstance();
                Map<String, String[]> parameterMap = request.getParameterMap();
                BeanUtils.populate(obj,parameterMap);
                args[index] = obj;
            }



        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | IOException e) {
            throw new RuntimeException(e);
        }
    }
    public void handlerListType(HttpServletRequest request,ParameterDefinition parameterDefinition,Object[] args,int index){
        Map<String, String[]> parameterMap = request.getParameterMap();
        //key --- Value
        //user[0].username --- new String[]{"pengyuyan"};
        //user[0].password --- new String[]{"123123"};
        //user[1].username --- new String[]{"wuyanzu"};
        //user[1].password --- new String[]{"123456"};
        

        //获取泛型类型
        Type pt = parameterDefinition.getActualTypeArguments()[0];
        String className = pt.getTypeName();
        Class<?> aClass = null;
        try {
            aClass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        ArrayList<Object> list = new ArrayList<>();
        Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();
        for (Map.Entry<String, String[]> entry : entries) {
            String key = entry.getKey();//user[0].username
            String[] value = entry.getValue();//new String[]{"guodan"}
            int i = Integer.parseInt(key.substring(key.indexOf("[") + 1, key.indexOf("]")));//集合中的下标
            String fieldName = key.substring(key.indexOf(".") + 1);//集合中元素对象的属性名
            String fieldValue = value[0];//获取集合中元素对象里的属性值

            Object o =null;
            try{
                o = list.get(i);
            }catch (IndexOutOfBoundsException e){
                //该集合下标上没有元素
                try {
                    o = aClass.newInstance();//创建对象
                    list.add(o);//添加对象
                } catch (InstantiationException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }

            }
            try {
                    BeanUtils.setProperty(o,fieldName,fieldValue);//设置对象属性
                } catch (IllegalAccessException | InvocationTargetException ex) {
                    throw new RuntimeException(ex);
                }
                args[index] = list;


        }
    }
    public Object[] handlerJavaType(HttpServletRequest request,String name,Class<?> clazz,Object[] args,int index){
        String parameter = request.getParameter(name);
        if(clazz==byte.class || clazz==Byte.class){
            args[index] =  Byte.parseByte(parameter);
        }else if(clazz==short.class || clazz==Short.class){
            args[index] = Short.parseShort(parameter);
        }else if(clazz==int.class || clazz==Integer.class){
            args[index] = Integer.parseInt(parameter);
        }else if(clazz==long.class || clazz==Long.class){
            args[index] = Long.parseLong(parameter);
        }else if(clazz==float.class || clazz==Float.class){
            args[index] = Float.parseFloat(parameter);
        }else if(clazz==double.class || clazz==Double.class){
            args[index] = Double.parseDouble(parameter);
        }else if(clazz==char.class || clazz==Character.class){
            args[index] = parameter.toCharArray()[0];
        }else if(clazz==boolean.class || clazz==Boolean.class){
            args[index] = Boolean.parseBoolean(parameter);
        }if(clazz==String.class){
            args[index] = parameter;
        }
        return args;
    }
    public boolean judgeTypeIsJavaOrNot(Class<?> clazz){
        if(clazz==byte.class || clazz==Byte.class || clazz==short.class || clazz==Short.class || clazz==int.class || clazz== Integer.class || clazz==long.class || clazz== Long.class || clazz==float.class || clazz== Float.class || clazz==double.class || clazz==Double.class || clazz==char.class || clazz== Character.class || clazz==boolean.class || clazz==Boolean.class || clazz==String.class){
            return true;
        }
        return false;
    }


}

其中的细节和难点:

1.处理参数

处理参数的思路

获取参数的class对象、参数的名字、参数的下标。然后根据参数名利用请求从前端获取对应的参数对象并添加到参数数组args[]中,位置为对应下标的位置。

其中需要根据不同的参数类型分别进行处理

如果是Java常用数据类型(8大基本数据类型+string),则进行装箱处理(String 直接添加),然后添加到参数数组args[]中;

public Object[] handlerJavaType(HttpServletRequest request,String name,Class<?> clazz,Object[] args,int index){
        String parameter = request.getParameter(name);
        if(clazz==byte.class || clazz==Byte.class){
            args[index] =  Byte.parseByte(parameter);
        }else if(clazz==short.class || clazz==Short.class){
            args[index] = Short.parseShort(parameter);
        }else if(clazz==int.class || clazz==Integer.class){
            args[index] = Integer.parseInt(parameter);
        }else if(clazz==long.class || clazz==Long.class){
            args[index] = Long.parseLong(parameter);
        }else if(clazz==float.class || clazz==Float.class){
            args[index] = Float.parseFloat(parameter);
        }else if(clazz==double.class || clazz==Double.class){
            args[index] = Double.parseDouble(parameter);
        }else if(clazz==char.class || clazz==Character.class){
            args[index] = parameter.toCharArray()[0];
        }else if(clazz==boolean.class || clazz==Boolean.class){
            args[index] = Boolean.parseBoolean(parameter);
        }if(clazz==String.class){
            args[index] = parameter;
        }
        return args;
    }
    public boolean judgeTypeIsJavaOrNot(Class<?> clazz){
        if(clazz==byte.class || clazz==Byte.class || clazz==short.class || clazz==Short.class || clazz==int.class || clazz== Integer.class || clazz==long.class || clazz== Long.class || clazz==float.class || clazz== Float.class || clazz==double.class || clazz==Double.class || clazz==char.class || clazz== Character.class || clazz==boolean.class || clazz==Boolean.class || clazz==String.class){
            return true;
        }
        return false;
    }

如果是请求/响应类型的数据则直接添加到数组中;

如果是数组,则获取的是数组对象,并添加到args[]中;

String[] arr = request.getParameterValues(name);
args[index] = arr;

如果是List集合类型(前端设计页面的时候会规范返回的格式),则要先通过获取泛型(这里需要回到参数描述类中添加泛型类型这一属性)来获取集合里面元素的类型,然后获得传入的数据map,遍历每行数据拼接出元素的名字、下标和属性值。然后通过元素的类型利用反射创建类对象,创建List集合并添加对象,设置对象属性值(注意:这里有一个小技巧,利用try-catch先获取空集合里面的元素,在出现没有元素的异常后在catch中创建对象,并添加到list中。这样在同一个元素下标中的list位置就有了对象,就不会进入到异常处理中创建对象,而是在后续进行属性设置,直到下标改变再重新创建对象。),最后将List集合添加到args[]中;

参数类型描述类添加泛型属性(相应的需要修改监听器封装部分的代码):

@NoArgsConstructor
@AllArgsConstructor
@Data
public class ParameterDefinition {
   private String name;//参数名
   private Class<?> clazz;//参数类型
   private int index;//参数下标
   private Type[] actualTypeArguments;//参数泛型的数组
   

}

监听器相关部分:

if(parameters.length!=0){
    for (int i = 0; i < parameters.length; i++) {
        //获取参数上泛型数组
        Type parameterizedType = parameters[i].getParameterizedType();
        Type[] actualTypeArguments = null;

        try{
            ParameterizedType pt = (ParameterizedType) parameterizedType;
            if(pt!=null){
                actualTypeArguments = pt.getActualTypeArguments();
            }
        }catch (Exception e){

        }

        String parameterName = parameters[i].getName();//获取参数名
        Class<?> parameterType = parameters[i].getType();//参数类型
        int index = i;//下标
        ParameterDefinition parameterDefinition = new ParameterDefinition(parameterName, parameterType, index,actualTypeArguments);//封装参数描述类对象
        parameterList.add(parameterDefinition);
    }
}

DispatcherServlet:

 public void handlerListType(HttpServletRequest request,ParameterDefinition parameterDefinition,Object[] args,int index){
        Map<String, String[]> parameterMap = request.getParameterMap();
        //key --- Value
        //user[0].username --- new String[]{"pengyuyan"};
        //user[0].password --- new String[]{"123123"};
        //user[1].username --- new String[]{"wuyanzu"};
        //user[1].password --- new String[]{"123456"};
        

        //获取泛型类型
        Type pt = parameterDefinition.getActualTypeArguments()[0];
        String className = pt.getTypeName();
        Class<?> aClass = null;
        try {
            aClass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        ArrayList<Object> list = new ArrayList<>();
        Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();
        for (Map.Entry<String, String[]> entry : entries) {
            String key = entry.getKey();//user[0].username
            String[] value = entry.getValue();//new String[]{"guodan"}
            int i = Integer.parseInt(key.substring(key.indexOf("[") + 1, key.indexOf("]")));//集合中的下标
            String fieldName = key.substring(key.indexOf(".") + 1);//集合中元素对象的属性名
            String fieldValue = value[0];//获取集合中元素对象里的属性值

            Object o =null;
            try{
                o = list.get(i);
            }catch (IndexOutOfBoundsException e){
                //该集合下标上没有元素
                try {
                    o = aClass.newInstance();//创建对象
                    list.add(o);//添加对象
                } catch (InstantiationException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
                try {
                    BeanUtils.setProperty(o,fieldName,fieldValue);//设置对象属性
                } catch (IllegalAccessException | InvocationTargetException ex) {
                    throw new RuntimeException(ex);
                }
                args[index] = list;
            }


        }
    }

如果是Model类型(model需要创建,属性只有一个map,用来存放属性的名和值,同时有一个setAttribute()方法,即添加键值。model是用来跳转页面的,将前端的数据封装到自己的容器属性中,然后通过请求传递给前端。同时创建一个ModelAndView类,除了容器属性外还有一个字符串类型的路径,功能和model类似,但适用于需要动态指定跳转页面的复杂业务),则直接添加到args[]中;

public class Model {
    private Map<String,Object> maps= new ConcurrentHashMap<>();
    public void setAttributes(String key,Object value){maps.put(key,value);}

    public Map<String, Object> getMaps() {
        return maps;
    }
}

public class ModelAndView {
    private Map<String,Object> maps = new HashMap<>();
    private String viewName;
    public void setAttributes(String key,Object value){
        maps.put(key,value);
    }

    public Map<String, Object> getMaps() {
        return maps;
    }

    public void setMaps(Map<String, Object> maps) {
        this.maps = maps;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }
}

如果是自定义类型,则用反射创建对象,将数据利用BeanUtils工具类的populate方法将参数整合到类对象中,并添加到args[]中。

public void handlerOtherType(Class<?> clazz,HttpServletRequest request,Object[] args,int index){
        try {
            Object obj = clazz.newInstance();
            Map<String, String[]> parameterMap = request.getParameterMap();
            BeanUtils.populate(obj,parameterMap);
            args[index] = obj;

        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
2.处理返回值

思路

如果返回值不为空,即该方法有返回值,则要对返回值的类型进行判断,然后添加请求数据,再进行转发。

不同类型返回值的处理:

这里返回值有两种情况,本质是跳转逻辑的不同。一种是字符串类型的路径,controller层会将前端的数据封装到Model对象中,servlet就获取model的map属性,遍历里面的数据利用请求设置到页面,而返回的字符串路径直接用来跳转,一种是ModelAndView类型的对象,同样遍历map属性,将属性通过请求设置到页面,并利用类里面的路径属性进行跳转。

public void handlerReturnVal(Object returnVal,HttpServletRequest request,HttpServletResponse response,Model model) throws ServletException, IOException {
        if(returnVal.getClass()==String.class){
            //如果返回值是字符串,先添加请求数据,再跳转(转发)

            handlerRequestVal(request,model.getMaps());
            jumpPage(returnVal,request,response);
        } else if (returnVal.getClass() == ModelAndView.class) {
            //如果返回值是ModelAndView,添加请求后跳转(转发)
            ModelAndView modelAndView = (ModelAndView) returnVal;
            handlerRequestVal(request,modelAndView.getMaps());
            jumpPage(modelAndView.getViewName(),request,response);

        }
    }
	//遍历Model/ModelAndView类对象的容器属性,把属性数据添加到请求
    public void handlerRequestVal(HttpServletRequest request,Map<String,Object> maps){
        Set<Map.Entry<String, Object>> entries = maps.entrySet();
        for (Map.Entry<String, Object> entry : entries) {
            String key = entry.getKey();
            Object value = entry.getValue();
            request.setAttribute(key,value);


        }
    }
	//页面跳转
    public void jumpPage(Object returnVal,HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
        String str = (String) returnVal;
        request.getRequestDispatcher(str).forward(request,response);
    }
3.处理JSON数据

如果接收的是JSON格式数据,那么如何识别数据以及怎样传递数据呢?

**思路:**参数注解用来识别判断客户端传递过来的数据是否是JSON数据,需要将JSON格式的字符串解析成参数类型的数据;方法注解表示服务器传输过去的数据为JSON数据,需要将返回值解析成JSON格式的字符串。

具体实现:添加RequestBody注解用在参数上,和ResponseBody注解用在方法上。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBody {
}


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

DispatcherServlet中在创建参数数组的时候判断参数上是否有注解,所以要在参数描述类中添加用来判断是否参数有注解的布尔值。在方法描述类中添加布尔值判断方法上是否有注解。(相应的要修改监听器中封装类的那部分代码)

参数描述类和方法描述类修改:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class ParameterDefinition {
    private String name;//参数名
    private Class<?> clazz;//参数类型
    private int index;//参数下标
    private Type[] actualTypeArguments;//参数泛型的数组
    private boolean requestBodyHasOrNot;//参数上是否有@RequestBody注解

}


@NoArgsConstructor
@AllArgsConstructor
@Data
public class MethodDefinition {

    private String requestMappingPath;//子级URi
    private String name;//方法名
    private Method method;//方法对象

    private Class<?> returnClazz;//返回值类型
    private List<ParameterDefinition> parameterDefinitions;//参数描述类对象的集合
    private boolean responseBodyHasOrNot;//方法上是否有@ResponseBody注解
}

监听器相关部分:

if(parameters.length!=0){
    for (int i = 0; i < parameters.length; i++) {
        //获取参数上泛型数组
        Type parameterizedType = parameters[i].getParameterizedType();
        Type[] actualTypeArguments = null;

        try{
            ParameterizedType pt = (ParameterizedType) parameterizedType;
            if(pt!=null){
                actualTypeArguments = pt.getActualTypeArguments();
            }
        }catch (Exception e){
        }

        String parameterName = parameters[i].getName();//获取参数名
        Class<?> parameterType = parameters[i].getType();//参数类型
        int index = i;//下标

        //获取参数上是否有@RequestBody注解
        boolean requestBodyHasOrNot = false;
        RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
        if(requestBody!=null){
            requestBodyHasOrNot = true;
        }
        ParameterDefinition parameterDefinition = new ParameterDefinition(parameterName, parameterType, index,actualTypeArguments,requestBodyHasOrNot);//封装参数描述类对象
        parameterList.add(parameterDefinition);
    }
}

//获取方法上是否有@ResponseBody注解
boolean responseBodyHasOrNot = false;
ResponseBody responseBody = method.getAnnotation(ResponseBody.class);
if(responseBody!=null){
    responseBodyHasOrNot = true;
}
MethodDefinition methodDefinition = new MethodDefinition(sonUri, methodName, method, returnType, parameterList,responseBodyHasOrNot);//封装方法描述类对象

servlet中在自定义类型中要判断是否有对应的注解,如果有就解析json数据封装成对象然后添加到数组。

public void handlerOtherType(ParameterDefinition parameterDefinition,HttpServletRequest request,Object[] args,int index){
    try {
        if(parameterDefinition.isRequestBodyHasOrNot()){
            //如果返回的是JSON格式数据
            BufferedReader br = request.getReader();
            StringBuffer sb = new StringBuffer();
            char[] cs = new char[1024];
            int len;
            while((len= br.read(cs))!=-1){
                sb.append(cs,0,len);
            }
            String jsonStr = sb.toString();
            Object obj = JSON.parseObject(jsonStr,parameterDefinition.getClazz());
            args[index] = obj;


        }else{
            Object obj = parameterDefinition.getClazz().newInstance();
            Map<String, String[]> parameterMap = request.getParameterMap();
            BeanUtils.populate(obj,parameterMap);
            args[index] = obj;
        }



    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | IOException e) {
        throw new RuntimeException(e);
    }
}

在方法处理中判断是否有对应的方法注解,如果有就将返回的JSON转换为字符串写回前端。

public void handlerReturnVal(MethodDefinition methodDefinition,Object returnVal,HttpServletRequest request,HttpServletResponse response,Model model) throws ServletException, IOException {

        if(methodDefinition.isResponseBodyHasOrNot()){
            //返回数据为JSON格式
            String jsonString = JSON.toJSONString(returnVal);
            sendResponse(response,jsonString);

        }
        else if(returnVal.getClass()==String.class){
            //如果返回值是字符串,先添加请求数据,再跳转(转发)

            handlerRequestVal(request,model.getMaps());
            jumpPage(returnVal,request,response);
        } else if (returnVal.getClass() == ModelAndView.class) {
            //如果返回值是ModelAndView,添加请求后跳转(转发)
            ModelAndView modelAndView = (ModelAndView) returnVal;
            handlerRequestVal(request,modelAndView.getMaps());
            jumpPage(modelAndView.getViewName(),request,response);

        }
    }
    public void sendResponse(HttpServletResponse response,String jsonString) throws IOException {
        response.getWriter().write(jsonString);
    }
注意:

1.@WebServlet不要加,在web项目中配置。因为是在项目中使用到,而不是全局。

postman

代替浏览器发送请求的工具

请求里面输入url,body选择raw->JSON,发送一个JSON格式的字符串

测试能否成功。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值