Restful

在WebAPI开发中,有一个著名的REST (representational state transfer,表现层状态转移)概念,REST 概念由罗伊·菲尔丁于 2000年在他的博士论文中提出。REST 原则提倡按照HTTP 的语义使用HTTP,如果一个系统符合 REST原则,我们就说这个系统是 Restful 风格的Restful是 WebAPI设计中非常重要的一个概念,但是很多开发人员对于 Restful 的理解存在误区,本节将会对 Restful进行详细的讲解。

一、什么是Restful

WebAPI开发有两种风格:面向过程的(简称 RPC)、面向REST的(简称REST)。

在RPC风格的WebAPI中,我们通过“控制器/操作方法”的形式来调用服务器端的方法把服务器端的代码当成方法去调用。这种风格的接口可能会用 POST 请求处理所有的操作方法,无论是获取、新增、更新还是删除数据,这样的接口只是把 HTTP 当成一个传输数据的通道,而不关心HTTP 谓词的语义。在这样的接口中,我们通过QueryString(查询字符串)或者请求报文体来为服务器传递数据。只要服务器端能够正常完成客户端请求的处理,服务器就会统一返回200的HTTP状态码。对于逻辑上的错误,返回的HTTP状态码也是200,只不过在响应报文体中通过不同的错误码来表示,比如“获取的用户不存在”的错误码为 1,“没有权限获取这个用户”的错误码为 2。

在RPC风格的接口中,当需要加载所有用户的时候,我们就向/Persons/GetAll这个路径发送GET请求:当需要加载id=8 的用户的时候,我们就向/Persons/GetById?id-8这个路径发送GET请求;当需要更新id=8 的用户信息的时候,我们就向/Persons/Update这个路径发送POST请求,并且把新的用户信息以JSON 格式放到请求报文体中;当需要新增一个用户的时候,我们就向/Persons/AddNew 这个路径发送POST 请求,并且把要新增的用户信息以JSON格式放到请求报文体中:当需要删除id-8 的用户信息的时候,我们就向/Persons/DeleteById/8 这个路径发送POST请求。由此可见,在 RPC 风格的系统中,URL (uiform resource locator,统资源定位符)中包含以名词形式描述的资源(比如 Persons)和以动词形式描述的动作(比如AddNew)。

与之对应,在 REST 风格的 Web API 中,接口把服务器端当成资源来处理。REST 风格的接口按照 HTTP 设计之初的语义来使用 HTTP,把系统中的所有内容都抽象为资源,所有对资源的操作都是无状态的且可以通过标准的 HTTP 谓词来进行。

HTTP的设计哲学包含以下几个重点内容。

1、在HTTP中,我们要通过 URL 进行资源的定位。比如要取 d-888 的用户信息,我们就向/user/888 这个路径发送请求,而要取 id-888 的用户的订单列表,我们就向/user/888/orders 这个路径发送请求。

2、在 HTTP 中,不同的请求方法(又被叫作请求谓词)有不同的含义。主要的谓词有GET、POST、PUT、DELETE、PATCH、OPTIONS 等,我们这里只讨论用得比较多的GET.POST、PUT 和 DELETE。不同谓词有不同的用途,获取资源用GET、新增资源用POST、整体更新(如果不存在则创建)资源用 PUT、删除资源用 DELETE。我们不应该错误地使用谓词,比如删除一个资源的时候,我们不能使用 GET 请求,而应该使用 DELETE 请求。

3、在HTTP 中,DELETE、PUT、GET请求应该是幂等的,而POST 则不是幂等的。所谓“幂等”指的是:对于一个接口采用同样的参数请求一次和请求多次的结果是一致的,不会因为多次请求而产生副作用。例如,“发表评论”功能需要是幂等的,用户填写评论并且单击[发布]按钮,评论被插入数据库,但是在返回结果的时候由于网络等问题,用户没有看到“发布成功”的消息,因此用户又单击了一次[发布]按钮,如果最终用户只发布了一条评论,那么这个操作就是幂等的,而如果用户连续单击两次[发布]按就发布了两条评论,这个操作就不是幂等的。由于网络环境存在不稳定性,当遇到网络故障导致请求失败时,如果接口是幂等的,系统就可以向接口重新发送请求,而不用担心重复发送请求带来副作用。

4、在HTTP中,GET请求的响应是可以被缓存的,而DELETE、PUT、POST请求的响应是不可以被缓存的。客户端、网关等可以根据情况对 GET 请求的响应进行缓存,从而提升性能。

5、在HTTP中,服务器端要通过状态码来反映资源获取的结果。比如,客户端要获取id-8的用户,如果要获取的用户不存在,则服务器返回的状态码为 404,而如果当前客户端没有权限获取这个用户,服务器返回的状态码为 403。再如,对于新增用户请求,如果新增成功,服务器返回的状态码为 201。

在一个 Restful 风格的 Web API系统中,每一个控制器都是对一类资源的操作的集合,每个操作方法都被不同的HTTP 谓词触发。例如,我们把系统中的“用户”抽象成 Person 资源,就可以开发如下代码所示的用户控制器。

[Route("api/[controller]")]
public class PersonsController : ControllerBase
{
[HttpGet]
public IEnumerable<Person> GetPersons();

[HttpGet("{id}")]
public Person GetPerson(long id);

[HttpPut("{id}")]
public void UpdatePerson(long id,Person person);

[HttpPost]
public void SavePerson(Person person);

[HttpDelete("{id}")]
public void DeletePerson(long id);
}

上面的代码省略了方法的实现,我们主要关注方法的声明。当需要加载所有用户的时候我们就向/api/Persons 这个路径发送GET 请求添加了HttpGet]的 GetPersons 方法就会被调用当需要加载 id-8 的用户的时候,我们就向/api/Persons/8 这个路径发送 GET 请求,添加了[HttpGet("{id;")]的 GetPerson 方法就会被调用:当需要更新 id=8 的用户信息的时候,我们就向/api/Persons/8这个路径发送PUT 请求,并且把新的用户信息以JSON格式放到请求报文体中,添加了[HttpPut("id;")]的 UpdatePerson 方法就会被调用;当需要新增一个用户的时候,我们就向/api/Persons这个路径发送POST请求并且把要新增的用户信息以JSON格式放到请求报文体中,添加了[HttpPost]的 SavePerson 方法就会被调用;当需要删除id=8的用户信息的时候,我们就向/api/Persons/8 这个路径发送 DELETE 请求,添加了[HttpDelete("{idy")]的DeletePerson 方法就会被调用。由此可见,在 Restful 风格的系统中,URL中的单词都是名词动作通过HTTP谓词来表述。

需要注意的是,REST 风格和 RPC 风格没有好坏的区分。RPC 风格是业务驱动的产物更加自然,而REST风格要求开发人员对 REST原则更了解,并且有更高的设计能力。

二、Restful的优缺点

Restful更符合HTTP设计的语义,因此我们把接口设计成Restful风格有如下的优点。

1、所有的资源都尽量通过URL来表示,避免通过QueryString、报文体来对资源进行定位,这样URL的语义性更清晰。

2、对所有类型资源的新增、删除、修改、查询操作都统一为向资源发送POST、DELETE.

PUT、GET 请求,接口统一且具有自描述性,减少了开发人员对接口文档的依赖性。

3、对于 GET、PUT、DELETE 等幂等的操作,网关、网络请求组件等可以对失败的请

求自动重试。

4、网关等可以对 GET 请求进行缓存,能够提升系统的访问速度,而且降低服务器的压力。

5、通过HTTP状态码反映服务器端的处理结果,能够统一错误码,避免自定义错误码带来的不统一的问题。客户端也可以根据错误码进行统一处理,比如对于 403 状态码,客户端统一提示用户去登录。

6、网关等系统可以根据状态码来分析系统的访问数据,比如可以根据 HTTP状态码分析有

多少成功的请求,有多少失败的请求。

Restful 风格的接口虽然有很多优点,但是也有如下的缺点。

1、真实系统中的资源非常复杂,很难清晰地进行资源的划分,因此 Restful 风格对设人员的IT 技能和业务知识的水平要求都非常高。

2、真实系统中的业务很复杂,并不是所有的操作都能简单地对应到PUT、GET、DELETEPOST上。而且对于同一个资源的同一个 HTTP 谓词有时候有多个业务逻辑,比如“删除 id=8的用户”和“删除usermame=yzk 的用户”这两个逻辑,如果按照 Restful 风格,我们就要设users/8”和“users/username/yzk”这两个地址,这样的地址会让开发人员迷惑,开发人员可能习惯的仍然是“users/DeleteBvId/8”“users/DeleteByIdid=8”“users/DeleteByUserName/yzkusers/DeleteByUserName?userame=yzk”等能清晰地反映操作意图的地址。

3、真实系统是在不断进化的,一个操作最开始的时候被设计为幂等的 PUT,但是后来的版本又修改了逻辑,可能该操作就变成了不幂等的。如果调用者继续对这个操作进行重试可能会有副作用。

4、在 Restful 中,资源尽量通过 URL来定位,要尽量避免使用 QueryString 及请求报文体传递数据。比如要查询id=8的用户,我们就要使用“Users/8”这样的地址;要查询 name=yzk的用户,我们就要使用“users/name/yzk”这样的地址;如果要查询班级编号为 8并且年龄等于18岁的学生,我们就要使用“students/class/8/age/18”这样的地址。这样的地址格式是英文的表达方式,并不符合中文的表达习惯,因此这样写会让很多开发人员迷惑,习惯使用中文的开发人员可能更习惯通过QueryString或者通过请求报文体传递数据。

5、HTTP状态码的个数是有限的,特别是用于表示业务相关的错误码主要在 4xx状态码段中,而业务系统中的错误非常复杂,仅通过 HTTP状态码来反映错误有时候会无法满足要求。

6、有一些宽带运营商、路由器、浏览器会对非 200 状态码的响应报文进行篡改。比如作者就遇到过宽带运营商把状态码为 404 的响应报文篡改为了广告内容。当然,目前大部分正规的系统都是通过 HTTPS 部署网站的,因此不太可能遇到这个问题。

7、有的客户端是不支持 PUT、DELETE 请求的,比如旧版本的支付宝小程序、一些旧版浏览器等就不支持 PUT、DELETE 请求。据说,还有开发人员遇到过一些地区的小运营商的网络设备不支持 PUT、DELETE 请求的情况。

REST 是比较学术化的概念,它只是一个参考的风格,并不是一个必须遵守的规范。尽管Restful 接口有很多优点,但是也有很多缺点。项目开发中我们需要做取舍,并不一定需要严格遵守 Restful 风格。AWS、ElasticSearch 等的接口比较接近于 Restful 风格,不仅因为这些系统的开发人员是使用英语的,更因为这些系统的业务资源比较固定、业务流程变化不大。而很多互联网系统、业务系统比这些系统复杂很多,而且面临的使用场景也更加复杂,因此即使是腾讯、阿里巴巴等大公司的业务相关接口,很多也不是完全遵守 Restful 风格的。

REST 概念是用来指导我们设计接口的,而不是给开发带来麻烦的,不能因为要遵守Restful 风格而影响开发进度及系统的稳定。如果项目的资源及业务流程像AWS、ElasticSearch等比较清晰、固定,并且开发团队中有对 REST 理解非常深入的开发人员,那么我们可以严格遵守 Restful 风格。但是对于大部分系统,业务资源和业务流程都是非常复杂的,业务需求的变动也是比较频繁的,而且大部分项目的开发人员的技术是参差不齐的,如果严格遵守 Restfiu规范,会使得新员工的培养周期变长。因此在进行项目开发的时候,需要根据项目特点、公司人员等多方面情况,确定一个符合项目情况的定制版 Restful 规范。

三、Restful 中如何传递参数

在进行 Restful 接口设计的时候,我们需要考虑如何给服务器端传递参数。

在给服务器端传递参数的时候,有 URL、QueryString、请求报文体 3 种主要方式。通过URL 传递更符合 Restful 规范,但是如果要传递的参数太多或者内容太长的话,通过 URL 传递的方式就不太适合。通过 QueryString 传递比较灵活,但是同样不适合传递太长的内容。通过请求报文体传递参数不限制内容的长度,而且通过 JSON 可以传递复杂的格式,但是只有POST、PUT 支持请求报文体。按照 RFC 7231标准,GET、DELETE 请求中的报文体是未定义的语义,有的网络设备、软件、开发包会忽略 GET、DELETE 中的报文体,因此我们可以认为GET、DELETE 请求不能使用报文体。

在 REST 中,这3 种传递参数方式的意义是不同的。通过 URL 传递的参数主要用于对资源进行定位,比如资源的 ID、资源的分类ID 等。对于额外的数据,比如分页的页码等应该通过QueryString 传递。请求报文体应该用来供PUT和POST 提交主要数据,比如要更新 id-8的用户的姓名为“杨中科”,我们应该向/Users/8 这个路径发送PUT 请求,且请求的报文体为{name:"杨中科”,这样把要更新的用户的 id=8放到URL 中来对资源进行定位,通过请求报文体来告诉服务器具体的更新数据。

如果严格按照 Restful 风格,开发人员需要根据传递参数的不同用途来使用不同的传递方式。但是这很考验设计人员和开发人员对于 REST 的理解,理解不深入很容易导致传递方式混乱。因此,如果不能让 REST 概念在开发团队内全面且深入地贯彻,考虑到方便项目管理的目的,作者建议为项目中参数传递方式制定一个强制性、容易理解、容易实施的标准。

综上所述,作者对于 Web API 参数的传递建议如下:对于保存、更新类的请求一般都是使用 POST、PUT 请求,把全部参数都放到请求报文体中;对于 DELETE 请求,要传递的参数就是一个资源的 ID,因此把参数放到 QueryString 中即可;对于 GET 请求,一般参数的内容都不会太长,因此统一通过QuryString 传递参数就可以;当然对于极少数参数内容超过URI限制的请求,由于GET、PUT 请求都是幂等的,因此把请求改成通过 PUT 请求,然后通过报文体来传递参数。

四、返回错误码:200 派与 4xx 派的"对决"

我们之前讲到,在 Restful 风格中,服务器端都是通过 HTTP 状态码来返回服务器端的处理结果的。但是如果使用 HTTP 状态码来表示服务器端的处理结果,可能会带来很多麻烦。

HTTP状态码中用来表示用户自定义错误码的主要是4xx 段,这个段一共只有 100个状态码,其中还有 400、401、404 等保留的状态码,真正可用的只有几十个。而业务系统中的错误码是非常多的,比如微信公众平台中的公用错误码就有 160 多个。

HTTP状态码中的很多都有特殊的用途,比如404 代表请求的资源不存在,403 代表用户对于请求的资源没有访问权限。对于“获取 Id=8 的用户”这样的请求,如果服务器端不存在Id=8 的用户,服务器端是可以返回 404 状态码的。对于新增用户这样的操作,我们可以用400状态码告诉用户提交的数据有问题,但是无法区分“用户名为空”“用户名格式错误”“用户名已经存在”“邮箱为空”“邮箱格式错误”等不同的数据错误。

在 Restful 中,即使是“成功”也是分为不同的状态码的。普通的“成功”,服务器端会返回 200;成功创建了一个新的资源,服务器端会返回 201;成功了,但是没什么返回给客户端的,则服务器端会返回 204。如果严格按照 Restful 的规范,不仅接口的开发人员需要根据不同的处理结果设置不同的 2xx 状态码,而且客户端的开发人员也需要判断不同的“成功”状态码。

其实,HTTP 状态码并不适合用来表示业务层面的错误码,它是一个用来表示技术层面信息的状态码。比如在新增用户的操作中,如果服务器端要求提交JSON格式的数据,但是客户端提交的是XML格式的数据,服务器端应该返回400这个HTTP 状态码。但是如果用户提交的是合法的JSON格式的数据,只是提交的用户名格式不符合业务层面的要求,服务器端返回的状态码到底是 200 还是 400,这在业界是有争议的。

有的开发人员认为,既然服务器能够处理这个请求并且正常返回,就说明请求处理成功了服务器端应该返回200这个HTTP状态码,我们可以在响应报文体中再进一步解释错误。如新增用户失败,服务器端也要返回 200 这个HTTP状态码,业务层面的错误通过响应报文体中自定义的业务错误码来表示。我们把这一派的开发人员称为“200派”。

而有的开发人员认为,对业务层面的错误,服务器端也应该返回400、404 之类的HTTP状态码,这样更加符合 Restful风格。对于HTTP 状态码无法满足的业务层面的详细错误,服务器端可以通过响应报文体中自定义的业务错误码来表示。我们把这一派的开发人员称为“4xx派”

可见,对于通过响应报文体中自定义的业务错误码来表示业务层面请求错误这一点,开发人员是有共识的。比如对于新建用户时“用户名已存在”这样的错误,服务器端返回的报文体为“code":3,"message":"用户名已存在"。

两派主要的争议在于,对于业务层面的错误,服务器端返回的 HTTP 状态码应该是 200还是4xx。

“4xx派”有如下观点。

1、网关等中间系统可以监控 HTTP 状态码,对于频繁出现的非4xx、5xx 状态码可以发出警告,帮助运维人员尽早发现问题。客户端应该做好校验,避免把非法的数据提交给接口如果接口频繁出现 4xx 状态码,就说明客户端的代码不完善,需要优化客户端程序。如果对于业务错误不通过 4xx状态码告知网关,而是一味地返回 200,就会存在客户端使用体验极差但是运维人员却不能及时发现的问题。比如,浏览器会对 GET 请求进行缓存,用户第一次发送请求的时候,当前用户没有权限访问资源,服务器端返回的报文的响应体中是"code":6.message"."没有权限了,但是由于服务器端返回的状态码仍然是 200,浏览器就可能会把响应结果缓存;这样即使后面用户切换为有权限的用户登录,客户端再发出同样的请求时,浏览器有可能获取的还是{"code":6,"message":"没有权限”这个缓存的响应报文体。虽然说我们可以在服务器端控制缓存,但是这增加了开发的工作量。

2、HTTP 就是把 200当成“处理成功”,很多的系统都是按照 HTTP 状态码的不同含义进行设计的。由于业务的问题导致的处理失败也应该是一种失败,如果失败了服务器端返回的状态码还是 200的话,这会违背软件设计的初衷。

而“200派”有如下观点。

1、如果是数据库服务器连接失败、服务器内存不足、请求格式错误等问题,服务器端确实应该返回 5xx、4xx 这样的 HTTP 状态码。但是对于用户名已存在、用户名格式错误、余额不足等业务的错误,如果服务器端也返回 4xx 这样的HTTP状态码的话,会导致需要被关注的错误消息被淹没。而且系统中,我们应该把系统目志和业务日志区分开,业务层面的错误要通过业务日志来查看,两者不可混为一谈。

2、网络的问题归网络、业务的问题归业务。能够连接上服务接口并且执行完成,就应该认为已“执行成功”。为了简化开发,客户端一般都对4xx、5xx 等状态码统一显示“操作执行错误,请联系管理员”,这样我们的业务代码只需要对HTTP状态码为200的响应报文体进行解析,再报告业务错误。如果 4xx 状态码和业务错误码同时存在的话,需要客户端开发人员同时判断HTTP状态码和业务错误码,增加了客户端开发人员的工作量。

这两派都有不少的支持者。即使在大公司中,不同的公司也有不同的风格,比如百度公司的很多API的开发人员都是 200 派,而谷歌公司的很多API的开发人员都是4xx 派。甚至在同一家公司的不同产品中,同样具有不同的风格,比如微信支付的 API的开发人员是 4xx派而企业微信和微信小程序的API的开发人员则是2xx派。因此,不存在“最好的选择”,只要根据项目的情况选择一个适合自己的风格即可。

作者个人比较偏向于 4xx 派,但属于精简版的4xx 派,作者的观点如下。

1、如果操作能够正常执行完成,服务器端返回的HTTP状态码应该是200:对于数据库服务器连接失败、请求报文格式异常、服务器端异常等非业务错误,服务器端应该返回 4xx、5xx等状态码。

2、对于业务层面的错误,要使用 4xx 等 HTTP 状态码返回,比如对于需要认证身份但是没有提供身份的访问,服务器端就返回 401; 如果要访问的资源不存在,服务器端就返回 404.对于请求参数校验失败或者其他业务错误,服务器端就返回 400。除了提供 HTTP 状态码,也在响应报文体中给出详细的错误信息,比如"code":3,message":"用户不存在”》。这样做的好处就是,网关等中间系统可以通过 HTTP 状态码检测到处理错误,而客户端检测到HTTP状态码为200时,则认为请求处理成功,然后客户端从响应报文体中读取处理结果即可:如果 HTTP状态码为 4xx、5xx 等,则客户端只要直接读取并且解析响应报文体即可,如果解析响应报文体失败,客户端再提示“系统错误,请联系管理员”。

3、需要注意,对于业务错误,服务器端不仅要返 4xx 的 HTTP 态码,还要通过响应报文体给出详细的错误信息。作者之前就遇到过一个框架,当被请求的 d-8 的用户不存在村候,服务器就返回 404 的HTTP 状态码,但是没有给出其他信息,让作者一直以为这个局哥候户端访间的 URL 错误导致的,浪费了很长一段时同寻找问题。如果这个框架在通过 404表示用户不存在的同时,把响应报文体设置为("code"3-mesag""用户名已存在”,这样作就能尽早知道这个错误是因为用户ID 写错了,而不是因为 URL 格式写错了。

五、Restful 实现指南

我们已经了解了 REST的概念,以及在项目中如何根据项目情况合理地应用 REST概念本小节中,作者依据经验并结合 ASPNET Core 的技术特点,制定了一个如下风格的 Resthul实现指南。

1、对资源的操作使用RPC 风格,也就是所有操作的路径为“[controller]/[action]”这样的模式,比如增加用户的路径是“Users/AddNew”获取所有用户的路径是“Users/GetAlI”根据ID删除用户的路径是“Users/DeleteById”。接口风格统一,更容易使用和理解。

2、对于可以缓存的操作,使用 GET 请求;对于幂等的更新操作,使用PUT 请求;对于幂等的删除操作,使用 DELETE 请求;对于其他操作,都使用 POST 请求。如果公司里面调用接口的开发人员对 PUT、DELETE这样的请求抵触或者需要兼容不支持PUT、DELETE的客户端环境的话,也可以对可以缓存的操作使用GET 请求,其他操作都用POST请求。

3、参数的传递方式统一化。作者建议采用如下的规范:保存、更新类的请求使用POST.PUT 请求,把全部参数都放到请求报文体中;对于 GET 和 DELETE 请求,把参数放到QueryString中。

4、对于业务错误,服务器端返回合适的4xx段的HTTP 状态码不知道该选择哪个状态码就用400:同时,在报文体中通过 cde 参数提供业务错误码及错误消息。

5、如果请求的处理执行成功,服务器端返回值为200的HTTP状态码,如果有需要返回给客户端的数据,则服务器端把这些数据放到响应报文体中。

微软为WebAPI提供的模板代码、示例代码大部分都严格遵守 Restful风格,如果把它们改造成RPC风格,需要做如下操作。

1、控制器上添加的Route("[controller]")改为Route("[controller]/[action]")],这样[controller)就会匹配控制器的名字,而[action]就会匹配操作方法的名字。

2、通过不同的路由配置,ASPNET Core 中的控制器可以支持多个同名的重载操作方法但是配置不当会导致开发人员认为一个URL 请求应该调用 A 方法,但是却调用了A2方法因此为了避免麻烦,我们强制要求控制器中不同的操作用不同的方法名。

3、把[HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]这些 Attribute 添加到对应的操作方法上。这不仅会帮助接口开发人员明确操作方法接收的请求类型,更能帮助 Swagger+OpenAPl生成文档。

有一个需要注意的问题,在ASPNET Core Web API中,如果控制器中存在一个没有添加[HttpGet]、[HttpPost等的 public 方法,Swagger 就会报错“Failed to load APIdefinition.”。

对于这样的方法,请把[ApiExplorerSettingsIgnoreApi=true)]添加到方法上,从而告知 Swagger 忽略这个方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咬口大葱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值