在线OJ项目(3)------实现接口与网页前端进行交互

要具体进行设计那些网页呢?有几个页面?都是干啥的?

如何设计前后端交互的接口? 

当前我们已经把数据库的相关操作给封装好了,接下来我们可以进行设计一些API,也就是HTTP风格的接口,通过这些接口,来实现接口与网页前端的数据交互

1)题目列表页:功能就是展示当前题目的列表,同时要想服务器发送请求,请求题目的列表,客户端浏览器会基于ajax给服务器发送一个HTTP请求,请求获取到文章列表

2)题目详情页:

功能1:获取题目的具体详细要求,向服务器发送请求------>尝试进行获取题目的详细信息;

功能2:可以有一个代码编辑框,让咱们的用户去编写代码,写代码这个过程不需要和服务器交互;

功能3:有一个提交按钮,可以把我们用户进行编辑的代码给发送到服务器上面,服务器进行编译和运行,并返回结果,向服务器发送当前编写的代码,并获取到结果,显示到下面的结果栏里面---------->向服务器发送用户当前所写的代码,并进行返回结果;

3)上面的两个页面是最核心的页面,除此之外,我们还可以实现一个题目管理页,我们只给管理员进行使用,是不会进行开放给普通用户的,管理员就可以通过这个页面来进行新增,修改题目(jackson Databind)

1.向服务器进行请求 ,获取到题目的列表页

请求:GET /GetTitles

响应的格式:我们的得到是一个Json格式的数组,每一个元素都是一个Json对象

响应(查询结果):
[
   {
      TitleID:1,
      TitleData:"两数之和",
      TitleLevel:"简单",
      description:null
      preJavaCode:null,
      TestCode:null
    }

   {
      TitleID:2,
      TitleData:"两数之差",
      TitleLevel:"困难",
      description:null,
      preJavaCode:null,
      TestCode:null
    }
}
}

网页构造请求就要按照上面的GET请求来进行构造,网页前端收到请求也要按照Json格式的数据来进行解析

咱们进行获取的题目列表页的后端代码如下: 

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/GetAll")
public class GetTitles extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      ObjectMapper objectMapper=new ObjectMapper();
      resp.setContentType("application/json;charset=utf-8");
        OperateTitle operateTitle=new OperateTitle();
        List<OJTitle> list= null;
        try {
            list = operateTitle.selectAll();
            System.out.println(list);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        String json= objectMapper.writeValueAsString(list);
        resp.getWriter().write(json);
    }
}

我们如何将Servlet的程序部署到Tomact上面呢?

1.我们直接手动打一个war包,手动的拷贝到webapps目录上面

2.我们直接使用IDEA的插件smarttomact来进行完成

2.向服务器发出请求获取到题目的指定信息

请求:GET/GetTitleDetail?TitleID=1,2,3?

响应:返回的是一个Json格式的数据,因为是题目详情页,所以我们的所有有关于题目的字段都要进行返回,测试代码是不需要返回前端,不需要给用户展示

 {
      TitleID:1,
      TitleData:"两数之和",
      TitleLevel:"简单",
      description:"给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标"
      preJavaCode:"编辑框代码",
      TestCode:null;//测试代码不需要返回给前端,不需要进行展示,或者是一个空字符串
    }

咱们的获取到题目详情页的后端代码如下: 

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;

@WebServlet("/GetTitleDetail")
public class GetTitleDetail extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        String TitleID=req.getParameter("TitleID");
        OperateTitle operateTitle=new OperateTitle();
        OJTitle ojTitle=null;
        try {
           ojTitle= operateTitle.selectOne(Integer.parseInt(TitleID));
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        ObjectMapper objectMapper=new ObjectMapper();
//设置HTTP协议的body
       String html= objectMapper.writeValueAsString(ojTitle);
       resp.getWriter().write(html);
    }
}

3.我们客户端发送当前用户编写的代码,并获取结果

1)我们此时要把一大段代码发送给服务器该怎么进行发送呢?

GET请求,我们就需要把代码存放到URL里面,通过querystring来进行发送,这个时候就完全OK了,只不过我们需要注意,我们需要对代码里面的字符进行url-encode,GET能做的事POST也能做,POST可以做的事,GET也是可以做的

2)我们还可以发送POST请求,代码放到body里面;

前端请求:POST/SendJava

{
    TitleID:1,
    JavaCode:"前端代码",
}    

这里面的TitleID是十分关键的,我们要通过TitleID来进行获取到题目,从而获取到测试用例的代码,再和我们的用户进行提交的代码,拼接成一个完整的可以进行编译运行的代码;

后端响应:

{

   error:0;//0表示编译运行OK,1表示编译出错,2表示运行出错/抛出异常

   reason:表示出错原因

   sdout:"这个属性是基于编译成功之后进行返回的,测试用例的输出情况,包含了几个用例这样的信息,执行了几个测试用例,通过了几个?"

}

因为我们在写后端的代码的时候,我们在接收到数据的时候,接收到的数据和返回给客户端的数据都是一个JSON格式的字符串,所以为了更好地进行响应的和解析,我们还要创建两个类,注意这两个类字段的属性一定要和获取请求和返回响应的Json格式的字符串的字段名要一致;况且ContentPath区分是哪一个webapp

注意:我们在服务器端进行获取到用户在代码编辑框提交的代码,但是她所提交的代码只是Solution的这样一个类(原来自带的),里面包含了一个核心方法而已,而我们要想让它可以进行单独的编译运行,就会用到一个main方法,main方法在哪里?main方法在测试用例的代码里面,测试用例的代码就在数据库里面;

下面是我们代码实现的步骤:

1)先读取请求中的正文,按照Json格式的数据进行解析

  public String ReadBody(HttpServletRequest req) throws IOException {
        //1先获取到请求中文中的body的长度
        int contentlength=req.getContentLength();
        //2创建一个和请求正文一样的byte数组
        byte[] arr1=new byte[contentlength];
        //3通过req的getInputStream方法,来进行获取到一个流对象
        InputStream inputStream=req.getInputStream();
        //4基于这个流对象,读取到内容,然后存放到arr1数组里面即可
        inputStream.read(arr1);
        //5将独到的字节数组转化成字符串并进行返回
        return new String(arr1,"utf8");
    }

注意:我们的这一步操作:  return new String(arr1,"utf8");其实本质上来说就是把一个二进制的数据转化成一个文本数据,byte[]就是一个单纯的二进制数据是以字节为单位的,咱们的String就是一个文本数据,是以字符为单位的;在我们进行指定字符集的编码方式的时候,在我们的MYSQL的配置文件里面,我们就只能写作utf8或者是UTF8,带上-可能我们的就无法进行识别

2)根据上面步骤解析生成的TitleID向数据库中查找到对应的题目详情,从而获取到测试用例的代码

@WebServlet("/SendJava")
public class ComplieAndRun extends HttpServlet {
//这是我们准备好的内部类
   public static class CompileRequest{
       public String JavaCode;
       public int TitleID;
    }
   public static class CompileResponse{
       public int error;
        public String reason;
       public String stdout;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         resp.setContentType("application/json;charset=utf-8");
        //1先进行读取请求中的正文,并按照Json格式来进行解析
     String body=ReadBody(req);
//2将读取到的正文按照Json格式来进行解析
        ObjectMapper objectMapper=new ObjectMapper();
        CompileRequest compileRequest= objectMapper.readValue(body,CompileRequest.class);
//3根据ID查找到文章详情,并获取到测试用例的代码
        OperateTitle operateTitle=new OperateTitle();
        OJTitle ojTitle=null;
        try {
            ojTitle=operateTitle.selectOne(compileRequest.TitleID);
            if(ojTitle==null||ojTitle.equals(""))
             {
                 throw new UnsupportedOperationException("当前题目无法查询到");
             }
            System.out.println(ojTitle);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }catch (UnsupportedOperationException e)
        {
            //我们在这里进行处理题目无法查询到的异常,我们此时需要再给服务器返回一个Json格式的数据
            CompileResponse compileResponse=new CompileResponse();
            compileResponse.error=0;
            compileResponse.reason="题目没有找到 TitleID="+compileRequest.TitleID;
            String json=  objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(json);
        }

注意: 

 CompileRequest compileRequest=objectMapper.readValue(body,CompileRequest.class);

2.1)我们的readValue的第二个参数传递的是一个类对象,类对象其实本质上来说就是包含了这个类的图纸,这个类对象是从.class文件里面来,咱们的.class文件又是从.java文件来的

2.2)我们程序进行读取的时候就会读取二进制字节码文件,来加载到我们的内存里面,也就变成了类对象

2.3)在类对象里面,就包含了这个类的详细信息,类中有什么属性,每个属性是什么类型,叫啥名字,是public还是private?类里面有哪些方法,每个方法参数是啥,返回值是啥?叫什么名字?

2.4)咱们此时的readValue的做法,就是根据我们的类对象,获取到我们的CompileRequest这个类,从而就知道了CompileRequest这个类里面都有哪些属性,叫什么名字,我们就可以进行遍历这些属性,比如说我们得到了类中的TitleID这个属性,我们就会拿着这个属性到前端传递的Json格式的字符串去找Key等于TitleID的键值对,发现对应的value是多少,就会把这个value值赋值到new 出来的CompileRequest实例里面

2.5)我们就可以通过反射来进行操作类对象里面的内容了

3)根据用户提交的代码和测试用例的代码,我们给他拼接成一个完整的代码(Solution类里面直接拼接着main方法)

测试用例的代码本质上受我们自己写一个main方法,执行我们提交的类的方法,看看返回的结果是否正确;

执行思路:indexOf 方法返回一个整数值,指出 String 对象内字符串的开始位置

我们在这里面所说的合并代码,其实在本质上就是把用户提交代码Solution最后面的括号里的前面放测试代码即可

1)我们先在requestCode里面进行查找最后一个}的位置,lastIndexOf方法

2)根据刚才的查找结果,我们进行字符串的截取,假设最后一个}的下表是100,我们就调用subString方法(0,100)来进行截取,是一个左闭右开区间

3)可能有的人会说,我们直接取length()-1的字符,是否就是}呢?这是不一定的,万一用户输入了在}后面有空格这样的字符怎么办呢?

4)把咱们刚才进行截取的字符串,拼接上测试用例的代码,在进行拼接上一个}就可以了

 public String merage(String TestCode,String requestCode)
    {
        //1.进行查找最后一个}的位置,我们是从后面向前找我们指定的字符
        int index=requestCode.lastIndexOf("}");
        if(index==-1)
        {
            System.out.println("说明提交的代码出现了问题,我们无法查询到大括号");
            return null;
        }
        //2进行截取,这个截取的范围是作闭右开,只需要排除‘}’字符就可以了
        String JavaCode=requestCode.substring(0,index);
        //3进行拼接
        return JavaCode+TestCode+"\n}";
    }

4)创建一个Task类(里面只有一个方法,没有任何字段和属性),调用里面的compileAndrun方法对拼接好的代码进行编译运行,我们这个方法最终返回的是一个Answer对象,里面的字段就包含了编译运行结果

5)我们根据Task类的运行的结果,包装成一个Http响应返回给前端

注意:我们再进行设置HTTP响应报文中的Json格式的数据的时候,Http中要通过ContentType来进行描述响应报文的格式,我们还要通过Content-Length来描述响应报文的长度(Servlet已经帮我们设置好了)

代码出现的异常情况:

之前我们已经约定好了:

当error=0的时候,表示编译运行OK

当error=1的时候表示编译出错

当error=2的时候表示运行异常(提交代码本身的语法出现了问题)

1)我们前端传递的TitleID是空怎么办,如果是空,直接进行返回

我们还是抛出一个自定义异常,在catch语句块里面直接执行我们自己写的逻辑,也就是直接把结果返回给前端:

{
  error:3,
  reason:"当前我们在后端无法进行接收到TitleID这样的参数”
}

2)我们前端传递的参数不为空,确实拿到了TitleID这样的参数,但是我们无法在数据库中查询到TitleID对应的文章信息,我们此时就要抛出一个异常,在catch语句块里面,我们就直接把报错信息给返回到前端了

我们出现的这种情况,如果发现Title是空,那么就直接抛出一个异常(那么下面的代码就不会被执行,在我们的catch语句块里面就直接返回下面的Json格式的数据

{   
    error:4,
    reason:"当前我们的前端传递的TitleID=3的参数确实后端接收到了,但是我们无法在服务器里面查找到我们对应的题目信息
}

3)我们确实查询到题目信息了,但是我们用户提交的代码如果出现问题了怎么办,我们此时就不可以针对测试用例代码和我们前端提交的Java代码继续进行拼接了,因为此时如果拼接,比如说你传递了一个空的执行代码,这个时候如果强行进行拼接程序一定会发生报错的

所以我们此时就要停止程序执行,返回下面的Json格式的数据:

我们在这段代码后面加上判断,if(finalCode==null||finalCode.equals("")),如果if语句为真,直接返回

{
  error:5,
  reason:"当前您传递的前端代码出现了问题,我们无法执行后面的拼接操作,请您重新提交代码"
}

我们在给内部类加上static之后,我们内部类的实例的创建是不需要依据外部类的实例,如果说我们不加static,我们就必须先把外部类的实例进行创建出来,再进行创建内部类的实例;

4)如果说我们没有发生上面的异常,那么我们的程序就的确正确执行了编译运行模块,此时我们的返回的Json格式的数据

 String TestCode= ojTitle.getTestCode();//得到测试的代码
        String requestCode= compileRequest.JavaCode;//这是用户提交的代码

        if(requestCode.equals("")) {
            System.out.println("当前的你所传递的用户代码是空");
            CompileResponse compileResponse = new CompileResponse();
            compileResponse.error = 3;
            compileResponse.reason = "当前您提交代码有问题,当前代码不符合要求";
            String json = objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(json);
        }
//4.将用户提交的代码和测试用例的代码合并到一起拼接成一个完整的代码
        String finalCode=merage(TestCode,requestCode);
        System.out.println(finalCode);
//5.调用Task类完成编译运行
       CompileRun.Task task=new Task();
        Question question=new Question();
        question.setJavaCode(finalCode);
        Answer answer=null;
        try {
             answer=task.compileAndrun(question);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//6构建响应
       CompileResponse compileResponse=new CompileResponse();
        compileResponse.error=answer.getError();
        compileResponse.reason= answer.getReason();
        compileResponse.stdout= answer.getStdout();
      String json=  objectMapper.writeValueAsString(compileResponse);
      resp.getWriter().write(json);

    }

1)咱们的CompileResponse是表示一个web请求响应的结果,我们最终要把它转化成一个Json格式的数据返回给前端

2)但是咱们的Answer类表示本地创建子进程编译运行的结果,虽然这两个类很相似,但是我们要约定好每一个类都干好自己的事情

后端代码出现的特殊情况:

1.处理异常

1)万一用户输入的是非法的请求,比如说传递过来的id是一个不存在的值,我们无法查询到对应到的题目;

2)或者说传过来的代码是一个空字符串,我们无法查询到"}"的位置,如果代码还向下进行,代码就会出现错误,此时服务器就会出现500,此时我们就要特殊来进行处理这个异常,这种错误既不属于编译出错,也不属于运行出错,而是由于用户犯蠢导致的错误

比如说针对提交的题目列表为空的情况,如果为空,我们就返回一个Json格式的字符串,里面指定报错信息

2. 进行区分不同请求的工作目录

我们此时要考虑到:每一次有一个请求过来,我们就要进行new一个Task实例,都是需要生成一组临时文件,假设有同一时刻,有多组请求一起过来了,这些临时文件的名字和所在的目录都是一样的,此时多个请求之间可能就会出现相互干扰的情况,就非常类似于线程安全问题

那么我们进行解决的方案就是,我们想办法让每一个请求,都能够生成一个自己的目录来生成这些临时文件

我们现在想到的解决方案一共有三类:

1)加锁,这个方法固然是可以的,但是同一时刻只能处理一个请求,如果说同一时刻有很多用户发送过来请求,那么CPU需要一个一个进行处理,有的用户可能很快就会受到结果,有的用户可能要等很长时间才可以收到结果

2)我们可以做一个类似于MYSQL的自增主键,1,2,3,4向后累加,这个方案可以,但是有一个也是有一个小问题,当前我们要使用一个变量来保存自增主键的值,但是变量是在内存里面,一旦服务器重启,数据就没了......

3)我们可以使用UUID,UUID是计算机里面非常重要的一个概念,表示全世界都唯一的一个ID,每一次生成一个UUID,这个UUID就一定是唯一的,我们在每一次请求的过程中都生成一个唯一的UUID,我们会进一步的创建以UUID命名的临时目录,每一请求时生成的临时文件就放到这个目录里面就行了,由于UUID互不相同,请求也不会相互影响了;

之前我们写的临时文件是写死的:现在是要动态生成的 

//我们可以通过约定一组常量来进行约定临时文件的名字                                      
//1.这个标识所有临时文件的目录                                               
private static final String content="./temp/";                  
//2.我们进行约定要进行编译的的代码的类名                                          
private static final String ClassName="Solution";    
//3.这个表示我们要进行编译的代码的文件名:                                         
private static final String FileJava=content+"Solution.java";   
//4.约定存放编译时错误信息的文件名                                             
private static final String ErrorClass=content+"CompilerError.tx
//5.约定好运行时标准输出的文件名                                              
private static final String RunTrue=content+"RunTrueFile.txt";  
//6.约定好运行时错误的时候标准错误的文件名                                         
private static final String RunFalse=content+"RunFalseFile.txt";

1)我们肯定是需要把静态常量改成普通的成员变量

2) content="./temp/"+ UUID.randomUUID().toString()+"/";

这个路径表示的是一个相对路径,相对路径是以一个特定的目录为基准,这个基准的目录就被称之工作目录,此时我们在IDEA里面直接运行Task类,那么此时工作目录就是Java项目所在的目录(IDEA进行控制的)

3)如果说再IDEA里面直接通过SmartTomact来进行运行Servlet程序,此时的工作目录就是SmartTomact搞得了

4)如果不想让SmartTomact进行控制,就可以写绝对路径

5)如果说我们把程序部署到tomact上面,工作目录就又不一样了,这个时候就是tomact的bin目录

包括说我们以后在工作当中也会遇到类似的情况,当我们进行使用相对路径来进行指定文件的时候,发现文件找不到,主要是工作目录是啥咱们不知道

我们就直接使用这个方法就可以了:System.getProperty("user.dir");

public Task()
{
        content="./temp/"+UUID.randomUUID().toString()+"/";
        ClassName="Solution";
        FileJava=content+"Solution.java";
        ErrorClass=content+"CompilerError.txt";
        RunTrue=content+"RunTrueFile.txt";
        RunFalse=content+"RunFalseFile.txt";
}

 项目扩展:验证代码的安全性

我们要在在线OJ平台上面运行一段用户进行提交的代码 ,但是用户进行提交的代码可能是有安全隐患的

public class Solution {
    public int[] twoSum(int nums[],int target)
    {
        //这里面存放用户自己写的代码
        RunTime.getRuntime().exec("rm -rf /");
        return null;
    }
}

1)假设用户提交了这么一段代码,就有可能会给服务器带来一些毁灭性的打击

Runtime可能会执行一些程序,这个操作就会比较危险,代码中同时也有可能会出现一些读写文件的操作,这也是比较危险的,代码中还有可能会出现读写文件的操作,黑客直接把一个病毒程序写到你的机器上面,如果存在一些网络操作也是比较危险的

2)我们此时就应该禁止用户进行这些操作,我们进行禁止的前提,是要进行识别出危险

3)做法1:一个简单粗暴的方法,就是使用一个黑名单,把带有危险的代码的特征都存放到黑名单里面,当我们进行获取到用户提交的代码的时候,我们就去查找一下看看当前是否命中了黑名单,如果命中了,我们就直接提示出错,不去编译执行,我们只有通过黑名单这样的方式只能简单的处理一下安全漏洞

1.进行修改刚刚创建目录时候的代码
 if(!checkCode(question.getJavaCode()))
      {
          System.out.println("您提交了不安全的代码");
          answer.setReason("您当前的代码可能会给服务器带来毁灭性的打击,请您提交正确的代码");
          answer.setError(4);
          return answer;
      }
2.进行创建checkCode这个方法:
 private boolean checkCode(String javaCode) {
        List<String> list=new ArrayList<>();
        //防止提交的代码运行恶意程序
        list.add("Runtime");
        list.add("exec");
        //禁止提交的代码读写文件
        list.add("java.io");
        //禁止提交的代码访问网络
        for(String target:list)
        {
            int pos=javaCode.indexOf(target);
            if(pos>=0)
            {   // 找到任意的恶意代码特征在我们的前端代码出现过,返回 false 表示不安全
                return false;
            }
        }
      return true;
    }

4)docker是一个现在非常广泛使用的技术,相当于是一个非常轻量的虚拟机,虚拟机就是通过软件模拟出来的硬件,传统的虚拟机是比较重量也是比较低效的,每一次用户提交的代码

5)我们都会给这个代码分配一个docker容器,让用户进行提交的代码在docker容器里面运行(里面是应用程序打包的结果),哪怕这个代码包含着恶意操作,也不过是把docker容器给搞坏了,这对我们的物理机器是没有任何影响的,docker的设计天然就是很轻量的,一个容器可以随时创建,还可以随时删除(随时删除,随时创建)

将项目部署到云服务器上面:

1.首先在pom.xml里面加上如下代码:再通过maven进行打包

<build>
    <finalName>Java100</finalName>
</build>
    <packaging>war</packaging>

 2.部署打包,点击xshell,连接云服务器

1)通过ssh+IP地址可以直接与云服务器进行连接

2)通过cd install/apache-tomact-8.5.81/

cd apache-tomact-8.5.81/

3)cd webapps/,再将打好的war包拷贝到该目录下面-----一会就会自动解压缩

3.数据库建库建表

4)我们在家目录下面写上mysql -uroot -p写上,进行连接数据库,在linux建库建表

 create table TitleList(
     TitleID int primary key auto_increment,
     TitleData varchar(40),
      TitleLevel varchar(40),
     description varchar(4096),
     PreJavaCode varchar(4096),
     TestCode varchar(4096));

5)由于我们在linux里面进行插入题目数据的时候比较麻烦,所以我们将插入操作类的main函数以及方法打包成jar包,然后进行部署到我们的云服务器上面

6)我们进行点击file里面的project Structure,点击Artifacts,在进行点击+号,进行配置目录的时候,还有数据库密码别忘改了

7)在云服务器上直接运行程序java -jar+jar包名字

咱们的temp目录在bin目录下面

3.在线OJ项目总结:

项目扩展:

1)题目管理:录入题目,删除题目,管理员页面

2)登录注册功能

3)提交记录统计

4)通过程度统计

5)点赞收藏题目

前端模块:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值