IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> RESTful API设计规范 -> 正文阅读

[网络协议]RESTful API设计规范

目录

一、目标?

二、URL定义格式

2.1?Scheme

2.2 Port

2.3?Path

前缀

资源地址

2.4 Query

三、资源表述

四、?资源名和资源值

4.1 命名规范

4.2 时间戳格式

4.3 枚举类型定义规范

4.4 标准字段

4.5 通用参数名

4.6?单复数

五、统一接口

六、响应状态码

200 OK

201 Created

202 Accepted

204 No Content

400 Bad Request

401 Unauthorized

403 Forbidden

404 Not Found

405 Method Not Allowed

409 Conflict

500 Internal Server Error

501?Not Implemented

503?Service Unavailable

七、批量操作

PATCH,用PUT替代

有原子性

无原子性

八、重载POST

九、自定义报头

十、授权

十一、参考链接


一、目标?

  1. 明确对外提供服务的API格式
  2. 明确系统资源划分的方式
  3. 明确URL定义格式
  4. 明确API请求和响应信息格式
  5. 明确错误码定义格式

二、URL定义格式

URI组成部分:

https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E6%A0%87%E5%BF%97%E7%AC%A6??????

 hierarchical part
        ┌───────────────────┴─────────────────────┐
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
  abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘
scheme  user information     host     port                  query         fragment

2.1?Scheme

客户端在通过API与服务端进行通信时,必须使用HTTPS协议,才能保证报文安全,除非调试接口,否则严禁使用HTTP。

2.2 Port

各微服务对外提供服务时需要统一API端口,如HTTPS统一使用443,也可以自定义使用其他端口。

2.3?Path

前缀

path部分以api开头,后跟微服务名、版本及模块名或资源地址,示例:

  • https://host:port/api/message/v1/notification

message表示消息微服务。

v1表示版本,以字母v开头,后跟数字,API若无法保持兼容,或无法在旧API上进行扩展,则应使用新版本API。

notification表示微服务内部的一个模块或者资源地址,名字及层级均可自定义,具体见下一节资源地址

资源地址

参考文档RESTful API设计基础知识-资源分类RESTful API设计基础知识-URI设计

在路径设计中需要遵守下列约定:

  • 资源命名全部小写且易读,可使用连字符(-)或下划线(_)进行分隔
  • 资源名不可以使用 (.) 和 (..),会被浏览器识别为相对路径。
  • 资源的命名需要符合RESTful风格,只有算法资源中可以存在动词,否则只能使用名词
    • 算法资源:在数据集上执行算法的结果的资源,注重从该动作的结果方面来考虑(如"地图上符合搜索条件的地点")。
    • 其他资源:为特别目的专门预定义的一次性资源、服务暴露的每一个对象所对应的资源。
  • 路径部分使用斜杠分隔符(/)来表达层次结构:/parent/child。
  • 路径部分使用逗号(,)来表示非层次结构:/parent/child1,child2。
  • 采用OAS 3.0描述的API,必须遵循路径参数序列化规则:Parameter Serialization,统一使用下图红框中的风格。
    为防止URL过长被截断,服务端收到请求后需要检测序列化后的路径参数是否和请求body中对应字段匹配(个数是否相等、内容是否相同),不匹配则抛错处理。

示例如下:

  • https://host:443/api/micro-service/v1/file/{docid}/download-info
  • https://host:443/api/micro-service/v1/earth/37.0,-95.2?
  • https://host:443/api/micro-service/v1/file/items_count,items

2.4 Query

若路径变量与标点符号均解决不了问题,或者如果你要往一个算法里代入参数的话,那么可以采用查询变量(query variables)。

如果两个URIs只在查询变量上有差别,这表明它们只是为同一算法设置了不同的参数。

用查询变量来表达算法的输入:GET /search?q=jellyfish&start=20。

参数设计需要遵守下列约定:

  • 参数名使用下划线分隔小写字母的方式命名。
  • 采用OAS 3.0描述的API,必须遵循查询参数序列化规则:Parameter Serialization,统一使用下图红框中的风格

示例如下:

三、资源表述

请求body、响应body中的数据,即是资源表述,描述资源地址所指向数据的样子,格式为json,只允许使用object和array,不允许直接请求或响应string、number等。

所有请求和响应的"Content-Type"必须和实际的数据格式保持一致,保证调试工具(如:Postman)、SDK生成工具、文档生成工具能够正确识别接口。

四、?资源名和资源值

资源地址、资源表述、query中的资源名和资源值均需遵循以下规则

4.1 命名规范

资源地址中的资源名,采用小写字母,使用连字符(-)下划线(_)分隔

资源表述、query中的资源名,采用小写字母,使用下划线(_)分隔

若同一资源名会出现在资源地址、资源表述、query中,资源名必须一致,即统一使用下划线(_)分隔

4.2 时间戳格式

资源值为时间戳时,统一使用RFC3339格式。

4.3 枚举类型定义规范

资源值为枚举类型时,统一使用小写字符串,如有多级的情况,使用(?.?)进行层级区分。

message.new:关注是否有新消息产生

message.readed:关注是否有消息被标记已读

audit.apply:关注是否有新审核申请产生

audit.approved:关注是否有申请被审核

4.4 标准字段

使用一组标准资源名和资源值定义来确保相同的概念在不同API中具有相同的名称和语义。

4.5 通用参数名

现有已知通用的参数名如下表所示。

  • 分页搜索
使用位置参数名参数类型描述用法
queryoffsetint64开始响应的项目的偏移量

默认为0(>=0),小于0则抛错

未传offset字段,或未设置offset的值时,取默认值

limitint64每页最多可返回的项目数

默认20(可根据功能修改默认值),范围为 [1, 1000],超出范围则抛错

未传limit字段,或未设置limit的值时,取默认值

directionstring排序结果方向可选值:asc、desc
sortstring排序类型
response bodyentriesArray of objects条目列表
{
    "entries": [
       {...},
       {...}
    ],
    "total_count": 95
}
total_countinteger?<int64>总条数(忽略offset和limit)

4.6?单复数

表示多个数据的集合的资源名应使用复数,如:

  1. 若干个文件,/files
  2. 某个具体文件,/files/{id}

五、统一接口

参考文档:RESTful API设计基础知识-统一接口

对于资源的具体操作类型,由HTTP方法表示:

  • GET:用于获取关于资源的信息。
  • HEAD:与GET类似,但响应时只返回首部。
  • PUT:用于设定资源状态。服务器根据客户端发送请求时提供的表示来创建或修改资源的状态。
  • POST:用于新建从属资源,或用于往已有资源的状态里添加数据。
  • DELETE:用于删除资源。

其中,

  • 获取资源信息使用GET方法。
  • 新建/更新资源根据自身情况选择PUT或POST方法。
  • 删除资源使用DELETE方法。

HTTP方法和路径可能的组合如下:

请求方法

URL

描述

GET/department获取所有部门信息
POST/department新增一个新的部门
GET/department/{departmentid}获取指定部门详细信息
PUT/department/{departmentid}更新指定部门信息
DELETE/department/{departmentid}删除指定部门

对HTTP方法的使用,一定要遵循相应方法的安全性和幂等性

六、响应状态码

所有的API响应必须遵守HTTP规范(rfc7231),选择合适的HTTP状态码,常见的HTTP状态码如下表所示。

状态码

描述

1xx信息性状态码
2xx成功状态码
3xx重定向状态码
4xx客户端错误状态码
5xx服务端错误状态码

只有来自客户端的请求被正确处理后才能返回2xx的响应。

当API调用失败时,必须返回出错时的详细信息,信息放在响应body中返回,参数定义如下:

参数

类型

是否必需

描述

codestringY错误码。
descriptionstringY错误描述。
solutionstringN错误处理建议。
detailobjN

错误细节。

linkstringN错误信息地址。

参考链接:rfc7807

OAS 3.0中Error定义如下:

"Error": {

????"description":?"接口调用错误信息结构基类",

????"required": [

????????"code",

????????"description"

????],

????"type":?"object",

????"properties": {

????????"code": {

????????????"description":?"错误码",

????????????"type":?"string"

????????},

????????"description": {

????????????"description":?"错误描述",

????????????"type":?"string"

????????},

????????"solution": {

????????????"description":?"错误处理建议",

????????????"type":?"string"

????????},

????????"detail": {

????????????"description":?"错误细节",

????????????"type":?"object"

????????},

????????"link": {

????????????"description":?"错误信息地址",

????????????"type":?"string"

????????}

????}

}

每个API具体可能返回的错误码,在该API各个响应状态码对应的返回示例中列举说明

OAS 3.0中错误码返回示例列举定义如下,注意examples对象中的key,如下列中的Err400001000,OAS 3.0规范要求必须为英文:

"400": {

????"content": {

????????"application/json": {

????????????"schema": {

????????????????"$ref":?"#/components/schemas/Error"

????????????},

????????????"examples": {

????????????????"Err.Efast.InvalidRequest": {

????????????????????"$ref":?"#/components/examples/Err.Efast.InvalidRequest"

????????????????},

????????????????"Err.Efast.NoSuchObject": {

????????????????????"summary":?"设置所有者不存在",

????????????????????"value": {

????????????????????????"code":?"Efast.NoSuchObject",

????????????????????????"description":?"执行编辑自定义文档库操作,由于所传用户不存在,设置文档库所有者失败。"

????????????????????????"solution":?"检测所传参数,确保所传用户存在"

????????????????????}

????????????????}

????????????}

????????}

????},

????"description":?"请求错误"

}

错误码返回示例如下:

{

????"code":?"DocShare.OperationConflict",

????"description":?"执行批量配置权限操作,由于部分配置用户已有相关审核在处理,对这些用户配置权限失败",

????"detail": {

????????"message":?"列举配置权限失败的用户id",

????????"conflicts": {

????????????"accessor_ids": [

????????????????"4caf9d76-77b5-11e9-a7af-005056af0410",

????????????????"55313ea0-77b5-11e9-98e0-005056af0410"

????????????]

????????}

????}

}

200 OK

200状态码是最常见的HTTP状态码,在所有成功的GET请求中,必须返回此状态码。HTTP响应实体部分直接就是数据,不要做多余的包装。示例如下:

1.获取单个资源详情

{

????"is_locked":?true,

????"locker_id":?"fd40b9f2-91e4-11e3-9466-5254000a13e9",

????"locker_account":?"欢欢",

????"locker_name":?"欢儿"

}

2.获取资源集合

[

????{

????????"client_mtime":?"2019-04-25T02:21:21.328982+00:00",

????????"rev":?"BCC857D5752346D0838BDD05C67CF3EF",

????????"modified":?1380502294452719,

????????"name":?"name",

????????"editor":?"user1",

????????"size":?41

????},

????{

????????"client_mtime":?"2019-04-25T10:21:21.328982+08:00",

????????"rev":?"CFD857D5752346D0838BDD05C67CF3EF",

????????"modified":?1380502344452719,

????????"name":?"name",

????????"editor":?"user1",

????????"size":?55

????}

]

3.获取多重信息

{

????"users": [

????????{

????????????"id":?"d22f7ec5-231f-35f5-a495-9194b66193e4",

????????????"account":?"abc01@eisoo.com",

????????????"name":?"爱白痴01",

????????????"mail":?"abc01@qq.com",

????????????"group_id":?"eec3fd5-2d1f-35a5-a425-9194bd629344",

????????????"group_name":?"联系人组01"

????????},

????????{

????????????"id":?"d7bdf7ec5-231f-35f5-a495-9194b66193e4",

????????????"account":?"abc02@eisoo.com",

????????????"name":?"爱白痴02",

????????????"mail":?"abc02@qq.com",

????????????"group_id":?"eec3fd5-2d1f-35a5-a425-9194bd629344",

????????????"group_name":?"联系人组02"

????????}

????],

????"groups": [

????????{

????????????"id":?"22f7ec5-231f-35f5-a495-9194b66193e4",

????????????"creater_id":?"zgd",

????????????"group_name":?"aabc02",

????????????"count":?10

????????},

????????{

????????????"id":?"dfsd7ec5-231f-35f5-a495-9194b66193e4",

????????????"creater_id":?"zyb",

????????????"group_name":?"aabc01",

????????????"count":?10

????????}

????]

}

201 Created

当服务器创建对象成功(如上传新文件)后,返回此状态码,响应实体描述和链接到新资源,Location报头指向新创建资源的地址?。

202 Accepted

请求已被接收且可能会成功执行,但服务器还未对其执行任何动作(如请求待审核)。

204 No Content

服务器已成功满足请求,并且响应有效内容正文中没有其他要发送的内容。

400 Bad Request

由于明显的客户端错误(如:参数格式错误,未提供所需参数,参数为空,参数超出范围等)而导致API请求失败,返回由此状态码构造的错误码。

当服务器无法从其他4xx类型的状态码中找出合适的来表示错误类型时,返回该状态码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.InvalidRequest

执行xxx操作,由于客户端未提供必须参数xxx,操作失败

执行xxx操作,由于客户端传参xxx类型错误,操作失败

执行xxx操作,由于客户端传参xxx结构不正确,操作失败

执行xxx操作,由于客户端传参xxx枚举不存在,操作失败

执行xxx操作,由于客户端传参xxx值不符合要求,操作失败

请客户端开发核对相关API,确保传参正确

401 Unauthorized

由于身份认证失败(如:token过期或不存在等)而导致API请求失败,返回由此状态码构造的错误码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.UnauthorizedAccess

执行xxx操作,由于xxx未授权或授权已过期,操作失败

请核实xxx认证信息,确保通过认证

403 Forbidden

由于逻辑错误、权限错误、不正确的操作等原因而导致API请求失败,返回由此状态码构造的错误码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.AccessDenied

执行xxx操作,由于xxx没有xxx权限,操作失败

执行xxx操作,由于xxx已被禁用,操作失败

请确保xxx拥有xxx操作权限

404 Not Found

由于服务器未找到?Request-URI 所指向的数据而导致API请求失败,返回由此状态码构造的错误码。

当?Request-Body?中的请求参数所指向的数据不存在时,不应返回404,而是400

例如:

情景1: Request-Method: "?GET?"???

? ? ? ? ? ?Request-URI:?"?https://{host}:{port}/api/efast/v1/doc-lib/custom/{id}?" ,其中{id}为一个变量,用于标识请求的数据。

情景2:?Request-Method: "?POST?"??

? ? ? ? ? ?Request-URI:"?https://{host}:{port}/api/efast/v1/doc-lib/custom?"? ??

? ? ? ? ???Request-Body:"?{ ...,"owner_id":?{id},...}?"? ??

在上述情景中,若服务器未找到 {id} 所标识的数据。 情景1会返回404状态码,情景2则返回400状态码。

注意:若服务器未找到 {id} 所标识的数据时无法区分{id}来自于 Request-URI 还是?Request-Body,则统一返回400状态码。

构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.NoSuchObject执行xxx操作,由于xxx对象不存在,操作失败

请确保URI指向对象存在

405 Method Not Allowed

请求方法对于原始服务器是已知的,但目标资源不支持,从而导致API请求失败,返回由此状态码构造的错误码。

此外,原始服务器必需在响应中生成 Allow 报头,其中包含目标资源当前支持的方法的列表。

构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.MethodNotAllowed

执行xxx操作,由于目标资源不支持xxx方法,操作失败

请确保目标资源支持请求方法

409 Conflict

由于与目标资源的当前状态相冲突,从而导致API请求失败,返回由此状态码构造的错误码。

此状态码常被用于用户可能可以解决该冲突并重新发送请求的场景,返回信息应该对冲突资源进行描述。

构造后的错误码格式示例如下:

code

description

solution

detail

{服务/模块名}.OperationConflict

执行xxx操作,由于xxx重复,造成冲突,操作失败

请确保冲突资源状态,尝试通过xxx解决冲突

{

"message": "冲突资源为xxx",

"confict_resources": ?["element1", "element2"]

}

500 Internal Server Error

由于服务器内部环境或逻辑错误而导致API请求失败,返回由此状态码构造的错误码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.InternalError

执行xxx操作,由于未知原因导致服务内部错误,操作失败

请先重新尝试,若还存在问题联系管理员

501?Not Implemented

服务器无法识别请求方法并且不支持任何资源,从而导致API请求失败,返回由此状态码构造的错误码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.NotImplemented

服务端未实现请求方法

请核实请求方法是否有误

503?Service Unavailable

由于服务器暂时不可用而导致API请求失败,返回由此状态码构造的错误码。构造后的错误码格式示例如下:

code

description

solution

{服务/模块名}.ServiceUnavailable由于当前服务端繁忙,请求失败服务器繁忙,请稍后重试

七、批量操作

PATCH,用PUT替代

因PATCH存在部分浏览器兼容性问题,使用PUT实现PATCH的效果

文件具有若干属性

GET /files/{file_id}/attr

{

????"name":?"xxxx",

????"parent":?"xxx",

????"size":?111

}

想修改其中的name、parent

使用PATCH实现

PATCH /files/{file_id}/attr

{

????"name":?"xxxx",

????"parent":?"xxx"

}

使用PUT实现,注意:url中的属性必须与表述中的属性一致

PUT /files/{file_id}/attr/name,parent

{

????"name":?"xxxx",

????"parent":?"xxx"

}

有原子性

同步实现方式:返回正常状态码,url过长时采用重载POST
异步实现方式:1. 创建任务、2. 获取结果
响应:正常格式

无原子性

同步实现方式:采用扩展状态码207(Multi-Status,非标准状态码,可能有浏览器兼容性问题,慎用)或者采用重载POST,url过长时采用重载POST

异步实现方式:1. 创建任务、2. 获取结果
响应:多状态格式

{id}:数据的唯一标识,必须参数

status:HTTP状态码,必须参数

header:HTTP报头,类型为object,可选参数

body:HTTP主体,类型为object或者array,可选参数

注1:这里的参数 {id} 用于标识数据,一般使用名为 "id" 的单一参数来标识。但其形式并不固定,根据实际情况可能会出现多个参数,甚至是一个Object的情况。

例如:在批量创建部门文档库的场景中,需先建立批量创建任务,再根据任务id来查询批量创建结果。在 "建立批量创建部门文档库任务" API中,主要分为两步来处理,先根据传入部门id和创建规则来筛选出需要创建部门文档库的关联部门id,然后对其分别执行创建操作。在查询批量创建结果时,若某部门文档库的创建操作还未开始(状态码202),而此时前端需要返回结果中含有关联部门名称以实现UI交互,故单一的资源id无法满足需求,故这时需要补充资源名称来对资源进行辅助说明。如下所示:

[

????{

????????"id":?"ac1f7ec5-231f-35f5-a495-9194b661da45",

????????"name":?"开发部",

????????"status":?201,

????????"header": {

????????????"Location":?"/api/efast/v1/doc-lib/department/gns%3a%2f%2fAC94C0A235F54557AB02CCBE7E17A046"

????????},

????????"body": {

????????????"name":?"开发部"

????????}

????},

????{

????????"id":?"12ac7ec5-231f-35f5-a495-9194b661da45",

????????"name":?"测试部",

????????"status":?202

????},

????{

????????"id":?"3eba7ec5-231f-35f5-a495-9194b661da45",

????????"name":?"产品部",

????????"status":?500,

????????"body": {

????????????"code":?"Efast.InternalError",

????????????"description":?"执行编辑部门文档库操作,由于未知原因导致服务内部错误,操作失败",

????????????"solution":?"请先重新尝试,若还存在问题联系管理员"

????????}

????}

]

上面信息为 "查询批量创建部门文档库" API的正常返回信息,数组中每一个元素都是一次创建部门文档库的结果。
在此API中,使用参数 "id" 和 "name" 两个字段来表示 {id}。 其中参数 "id" 为资源id,用于唯一标识资源,参数 "name" 为资源名称,对资源进行辅助说明,以便创建失败和处理中时前端能够根据参数 "name" 的值实现用户交互。

注2:body其实反应单次处理的结果,故其具体格式应与调用普通API结果类似,例如当此次处理抛错时(status为非200系列),body格式固定为调用普通API发生错误的返回结果(参数为"code", "message", "cause", "detail")。

[

????{

????????"id":?"11111",

????????"status":?200,

????????"header": {

????????????"etag":?"xxx",

????????????"modified":?"xx"

????????},

????????"body": {

????????????"result":?"xxx"

????????}

????},

????{

????????"id":?"22222",

????????"status":?400,

????????"header": {

????????????"etag":?"xxx",

????????????"modified":?"xx"

????????},

????????"body": {

????????????"code":?"xxx.InvalidRequest",

????????????"description":?"执行xxx操作,由于参数xxx非法,操作失败",

????????????"solution":?"请验证参数的合法性",

????????????"detail": {

????????????????"invalid_params":?"key"

????????????}

????????}

????}

]

八、重载POST

以下情况,可以采用重载POST,不过不要因此丢失面向资源的设计,具体见RESTful API设计基础知识-重载POST

  1. 批量操作无原子性,可能部分成功,部分失败,不采用扩展状态码207(Multi-Status)
  2. url长度超过浏览器、nginx等的限制,如:算法操作,分析一张图片中的人物时,将图片作为输入参数,全部放到query?string中会导致url过长;批量操作,对表格中大量单元格的操作
  3. 复杂操作,如:修改元数据模板

九、自定义报头

  • 报头名称需要采用全小写(参考RFC7540,由于HTTP/1.1?大小写不敏感,HTTP/2?和?HTTP/1.x?同样使用?ASCII?字符集,但?HTTP/2?头部必须使用小写。考虑协议更新演进问题,因此自定义标头需要使用全小写)
  • 报头名称需要携带默认前缀x-

  • 前缀也可使用x-aishu-,但是要注意避免可能的oem问题(建议不带 aishu 关键字,之前AB为华为定制的产品,华为直接在安装目录下 grep 爱数相关关键字,后来代码中所有的爱数相关的关键字都删掉了)

十、授权

使用OAuth 2.0的方式实现API授权,先请求授权,获取ACCESS_TOKEN,然后再通过该ACCESS_TOKEN来调用需要授权的API。

调用需要授权的API时,必须将ACCESS_TOKEN放在HTTP header中:"Authorization: Bearer ACCESS_TOKEN"。

十一、参考链接

  1. HTTP | MDN
  2. rfc7231
  3. rfc7807
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-09-30 12:17:32  更:2021-09-30 12:17:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:26:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码