0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入理解RESTful Api设计

Android编程精选 来源:Java建设者 作者:微笑刺客D 2022-06-14 15:43 次阅读

REST

REST(REpresentational State Transfer)是 Roy Fielding 博士于 2000 年在他的博士论文中提出来的一种软件架构风格(一组架构约束条件和原则)。在该论文的 中文译本 中翻译是"表述性状态移交"。

ee0b2168-eb0c-11ec-ba43-dac502259ad0.png

原则

  • 网络上的所有事物都被抽象为资源
  • 每个资源都有一个唯一的资源标识符
  • 同一个资源具有多种表现形式(xml,json 等)
  • 对资源的各种操作不会改变资源标识符
  • 所有的操作都是无状态的

资源(Resources)

资源是一种信息实体或者说是一个具体信息,能够被想象出名字。比如多个图书馆,那么便是可使用的图书馆资源,而图书馆内,多个楼层,那么便拥有了多个楼层的资源,各楼层提供了不同服务,那么服务也是资源。在互联网中,可以用一个 URI(统一资源定位符)指向它,每种资源对应一个特定的 URI(如同一本书,按照书页码去定位哪一页,目的是定位资源)。访问这个特定 URI 便获取到了这个对应的资源。

表述(REpresentations)

资源的表述是一段对于资源在某个特定时刻的状态的描述,通过表述捕获资源,并在组件间(客户/服务器)移交该表述。表述有多种格式,如 HTML/XML/JSON/纯文本/图片/视频/音频等。具体的表述格式,可以在 HTTP 请求头信息中用 Accept 和 Content-Type 字段指定,请求/响应方向的表述通常使用不同的格式。

状态移交(State Transfer)

对于组件间而言(客户/服务器),资源的请求是一个互动过程。通过表述捕获资源当前或是预期的状态,相当于获得了资源的状态。通过移交代表资源的表述,来将资源在组件的两者之间进行传递,进而改变应用状态。如当客户端获取了资源后,自身状态处于稳定,当再次获取资源后自身状态再次处于稳定。客户端操作并对服务端发起请求,在资源上执行各种动作而打破资源自身状态,达到客户端操作所期望状态。

RESTful Api

与 REST 相比多了一个 ful,就英语层面来说是一个形容词,RESTful 翻译成中文为“REST 式的”,满足了 REST 架构风格的应用程序设计的 Api 则便是 RESTful Api,即 REST 式的 Api。

以往 Api 设计

在 MVC 项目中,经常都是设计成动宾结构给 ajax 调用

/getCustomers
/getCustomersByName
/getCustomersByPhone
/getNewCustomers
/verifyCredit
/saveCustomer
/updateCustomer
/deleteCustomer

可有时却因为没有统一的规范,多人协作时,对于动词的描述上也没有统一,时长出现了类似如下的各类叫法,不能说这种情况有什么弊端,毕竟这种方式也是正常工作着。

/getCustomers
/getAllCustomer
/getCustomerList
/getPagedCustomer
/queryCustomers
/queryAllCustomers
/queryCustomerList
...

相比之下,RESTful Api 提供了更为标准化,规范化的 URL 写法。

设计规范

考虑 Api 设计时,URI 中不能有动词,URI 的目的是定位资源,而具体的对资源的操作,是借助 HTTP 的动词完成,与早期 Api 设计相比,本身的思路是不同的,原来更多的是考虑函数式编程或者叫做面向行为的服务建模,比如 RPC,远程调用一个函数,那么 Api 设计便是会考虑为动词名词格式,而对于 REST 风格来讲,是面向资源的服务建模。而对于资源而言,可以是对象、数据或是查询服务。

HTTP 动词

对于一个系统而言,对外提供的功能总体上划分为两类:

  • 获取系统资源,主要包括读取资源和资源描述信息。
  • 对系统资源进行变更,主要包括写入资源,对已有资源状态的变更,删除已有资源。ee2ccc96-eb0c-11ec-ba43-dac502259ad0.png

对于这其中使用到的一些动词,使用 HTTP 的动词描述来承担对资源执行的行为,动词通常使用以下几种。对于 HTTP1.1 规范中的其他几个动词(如 OPTIONS 等)则不再介绍。

  • GET: 获取目标资源。
  • HEAD: 获取(传输)目标资源的元数据信息。该方法与 GET 相同,但是不传递内容体。
  • POST: 创建新资源,对于复杂查询而言,提交查询表单给查询服务也是常用 POST 的(当然其他几个能做的它也能做)。
  • PUT: 替换已有资源(整体)。
  • PATCH: 修改已有资源(局部)。
  • DELETE: 删除资源。

URI

URI 作为统一资源标识符,其本质是标识资源,就像进入图书馆,任一本书都应具有在哪个楼层,哪个区域,哪个书架等等标识信息,来唯一确定这本书,于资源而言,更是如此,对于 URI 的设计,规范是使用名词来定位资源,比如常见的

GET/Api/Users/{id}

这样便按照 id 值,来唯一定位这一 Useree46a990-eb0c-11ec-ba43-dac502259ad0.png

对于资源的单复数格式,尽管规范是尽可能使用复数,但并没有说哪条纪律或是约束限制说一定要使用复数,这无需强制约束,按照自身统一即可。毕竟有些不可数名词,没有复数格式,那么还是沿用本身,而对于整体风格为复数下,却又显得格格不入。

面向资源

资源的组织决定着 URI 的展示方式,对于底层数据库而言,也许 Order 模型有若干张表来支撑存储,对外总体是提供着 Order 服务。这样一来,如果按照底层数据库表来考虑 Api 设计,则会陷入无尽的关系处理中,比如 Order 下有 OrderItem,OrderItem 下有 OrderItemAttachment,如果按照这个思路去实现 Api 设计时,那么 URI 的设计上则会存在多级情况。

POST/Api/Orders
POST/Api/Orders/{id}/OrderItems
POST/Api/Orders/{id}/OrderItems/{itemId}/OrderItemAttachments
POST/Api/Orders/{id}/OrderItems/{itemId}/OrderItemAttachments/{}/...
...

于数据库而言,表与表间构成了一张庞大的网,有时还不好找到定位资源的入口ee57e11a-eb0c-11ec-ba43-dac502259ad0.png

如果按照单表进行 URI 设计,那么则成了面向表服务建模,这又造成了底层的服务细节统统对外暴露,因此需要避免创建仅反映数据库内部结构的 API。

在领域驱动设计中,聚合这一概念,将具有强相关的实体和值对象纳入到一起,形成独立空间、业务逻辑内聚于聚合之中,同生共死。面向聚合进行 Api 设计,多级路由的嵌套结构缓和许多,如需求上考虑 Order 创建时一定需要有 OrderItem 的存在,那么则对于这两者而言是捆绑的关系,而对于 OrderItemAttachment 而言,不是必要的。

ee80037a-eb0c-11ec-ba43-dac502259ad0.png

那么则可以独立设计聚合(此处忽略底层数据库中表是如何设计的,仅考虑聚合),URI 的设计也围绕着聚合这一资源来进行,这样一来,URI 的设计便成了如下结构

POST/Api/Orders
{
"locationId":1,
"productIds":[
1,
2,
3
]
}

POST/Api/Orders/{id}/OrderItems
{
"productIds":[
4,
5,
6
]
}

POST/Api/OrderItemAttachments
{
"orderItemId":1,
"fileUrl":"xxx"
}

嵌套层级结构不会太深,因为太深的层级结构往往也意味着这个聚合的设计或许存在一点问题。

约束设计

对于 Post、Put、Patch 和 Delete 这些操作来讲,面向聚合设计 URI 基本可以有路可循。

比如以下一些常见的 URI

POST/Api/Orders
POST/Api/Orders/{id}/OrderItems
POST/Api/OrderItemAttachment
PUT/Api/Orders/{id}
PUT/Api/Orders/{id}/OrderItems/{itemId}
PUT/Api/OrderItemAttachments/{id}
PATCH/Api/Orders/{id}/Address
PATCH/Api/Orders/{id}/OrderItems/{id}/Amount
PATCH/Api/OrderItemAttachments/{id}/FileUrl
DELETE/Api/Orders/{id}
DELETE/Api/Orders/Batches
DELETE/Api/Orders/{id}/OrderItems/{id}
DELETE/Api/OrderItemAttachments/{id}
POST/Api/Invites/emailTemplate

PATCH/Api/Invites/{id}/Sendmail//Sendmail作为邮件服务资源
PATCH/Api/Notifications/{id}/MessageStatus
PATCH/Api/Notifications/MessageStatus/batches
PATCH/Api/Orders/{id}/OrderItem/{itemId}/PayStatus

POST/Api/Orders/exports//返回导出资源
POST/Api/exportServices//提交给导出服务资源
POST/Api/exportServices/Sendmail
POST/Api/InviteParseServices//提交给解析服务资源

...

当然也有一些夹杂着动词,习以为常的 Api 设计,如果习惯了,不想改变,仍然可以使用着动词(后续提到该部分违反约束),但若想改变,就得换个思路去考虑设计了

POST/Api/Account/Login
POST/Api/Account/Logout
POST/Api/Account/Register

比如,Login/Logout 操作的目标资源是什么?如果把登录的用户当作在系统中存储的资源来看便可以认为已上线的用户信息,取个资源名字,在线用户(onlineUser),然后对其执行行为。而对于 Register 来讲,则更是容易转换了,注册本身是对 Account 的操作行为,其本质是创建一个没有过的用户。那么直接去掉注册即可了,如认可改变可以按照如下设计,如仍习惯现有,则不改即可,并没有什么约束、纪律限制说一定要遵循。

POST/Api/Accounts
POST/Api/OnlineUsers
//如下需要结合Authorization,不直接在URI中传递参数
DELETE/Api/OnlineUsers

主要是对于查询类的操作,设计起来复杂一些,无论是实际开发中还是按照二八原则,大部分操作都是查询操作,并且查询起来天马行空。

先是以下简单的查询

GET/Api/Orders
GET/Api/Orders/{id}
GET/Api/Orders/{id}/OrderItems
GET/Api/Orders/{id}/OrderItems/{id}

//筛选
GET/Api/Orders?Name=xxx&LocationId=xxx
//分页
GET/Api/Orders?Page=1&Limit=10
//也可以拆分成如下两个此处资源为Page
GET/Api/Orders/Page?Page=1&Limit=10
GET/Api/Orders/PageCount?Page=1&Limit=10
//排序
GET/Api/Orders?Sort=Name%20DESC
GET/Api/Orders?Sort=Name%20DESC,CreationTime%20ASC

然后再为一些常见场景下的(对于查询类的,聚合的边界应消失了,更多的应该是将各种资源串联起来)

//UI上需要知道某个资源是否存在
GET/Api/Orders?name=xxx
HEAD/Api/Orders?name=xxx
能够查询到状态码返回204
找不到状态码返回404

//文件下载
GET/Api/OrderFiles/{id}/Url

//报表分析(将报表分析的结果作为虚拟资源)
GET/Api/AnalyseResults

//返回指定条件下的总数
GET/Api/Locations/{id}/OrderCount?Status[]&Status[]=2&CreationTime=2022-05-01

//UI上下拉框所需要的基础数据
GET/Api/Locations/Names?page=1&limit=30&search=xxx
{
"id":"xxx",
"name":"xxx"
}

//获取最近的循环周期
GET/Api/Plans/{id}/LatestCycleDate

//获取最近的记录(根据时间,状态过滤后的第一条)
GET/Api/Orders/Latest

...

实际使用中,算了算也只有百分之八十左右的接口是按照 RESTful Api 的规范使用着的,总是有些接口,不能或是难以用简单的描述就能解决。比如如下几个接口,我便直接违反着约束(不能有动词,只能使用名词)。

PATCH/Api/Invites/{id}/Approval
PATCH/Api/Invites/{id}/Decline
PATCH/Api/Invites/{id}/Reject
...

Github中也还是有动词描述https://docs.github.com/en/rest/codespaces/codespaces#start-a-codespace-for-the-authenticated-user

https://docs.github.com/en/rest/codespaces/codespaces#stop-a-codespace-for-the-authenticated-user

https://docs.github.com/en/rest/checks/runs#rerequest-a-check-run

https://docs.github.com/en/rest/checks/suites#rerequest-a-check-suite

如果按照这几个约束条件来看的话,仅当满足三个约束条件的才能认为是 RESTful Api,而满足一个或是两个约束条件的为 Http Api,那么我们或许是一直在追随 RESTful Api 的路上了。

ee8f2d64-eb0c-11ec-ba43-dac502259ad0.png

面对这部分难以描述或是无法组织的接口,个人认为直接违反一些约束即可,总归是只有少部分接口仅满足一个到两个约束。

状态码

HTTP 中使用状态码来表示着请求的成功与否,我们可以直接使用它,而无需在返回值中再包裹一层 code/message,尽管在 mvc 中,我也很喜欢这么做。

{
"code":200,
"message":"",
"data":{

}
}

对 HTTP 的状态码接触越多后,越发觉得思路偏了,不应该将请求响应的状态码与业务中行为的成功与否进行隔离开,因为 HTTP 本身是应用层协议(超文本移交协议),是为业务服务的。如何在网络层面上把一个请求发送出去,再接收到响应,这是 TCP 协议来保障的。假设网络层如果请求失败了,那么应用层都无法进行,因此结合状态码与返回内容(当出现异常时仍然返回状态码与错误描述信息)。如下 HTTP 的状态码覆盖了绝大部分场景。当客户端需要追踪问题时,查看对应请求的状态码,结合其对应的解释说明,便可以去定位相关的问题,当然,前提是真的返回了符合场景下的状态码。

  1. Informational responses (100199)
  2. Successful responses (200299)
  3. Redirection messages (300399)
  4. Client error responses (400499)
  5. Server error responses (500599)

在 Api 中,100 阶段的状态码不会涉及,具体的各响应码参见如下图

eedef5b0-eb0c-11ec-ba43-dac502259ad0.pngimg

版本号

对外提供的资源服务地址需要存在版本控制,以便于客户端应用能够访问到对应的资源,版本号的规划有如下几种方式,具体使用哪种得依靠具体的情况而分析:

  1. 不考虑版本,内部使用、短暂的生命周期下不考虑资源的变更或是直接对资源本身进行了换新如此变更到新的 url 上。
  2. 为每个资源的 URI 添加一个版本号。
GET/Api/v2/Orders/{id}
  1. 作为查询字符串参数来指定资源的版本
GET/Api/Orders/{id}?version=2
  1. 在 http 的 header 中增加自定标头设置版本号。
GET/Api/Orders/{id}
Custom-Header:version=2

成熟度模型

2008 年,Leonard Richardson 为 Web API 提出了以下 成熟度模型 :

  • Level 0: 定义一个 URI,所有操作是对此 URI 发出的 POST 请求。
  • Level 1: 为各个资源单独创建 URI。
  • Level 2: 使用 HTTP 方法来定义对资源执行的操作。
  • Level 3: 使用超媒体(HATEOAS: 「H」ypermedia 「A」s 「T」he 「E」ngine 「O」f 「A」pplication 「S」tate,参见 HATEOAS - Wikipedia )。

诚然,对于这个成熟度模型,我一般都只会去达到前三个级别,虽然 Roy Fielding明确表示 ,Level 3 才是真正的 RESTful Api,对于 Level 3 级别,其实并没有理解到其具体奥妙。因为我们面对的是 UI,用 UI 去链接操作,那么对于 Level 3 返回的超媒体而言,又如何表现呢?

原文标题:Restful 是什么??

文章出处:【微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 软件架构
    +关注

    关注

    0

    文章

    64

    浏览量

    10280
  • REST
    +关注

    关注

    0

    文章

    32

    浏览量

    9407
  • Restful
    +关注

    关注

    0

    文章

    11

    浏览量

    3532

原文标题:Restful 是什么??

文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    深入理解C语言:循环语句的应用与优化技巧

    能让你的代码更加简洁明了,还能显著提升程序执行效率。本文将详细介绍C语言中的三种常见循环结构——while循环、for循环和do...while循环,带你深入理解
    的头像 发表于 12-07 01:11 85次阅读
    <b class='flag-5'>深入理解</b>C语言:循环语句的应用与优化技巧

    socket 与 RESTful API 的使用

    在现代网络应用中,数据传输和通信是核心功能之一。为了实现这一功能,开发者通常会使用两种主流的技术:Socket和RESTful API。 1. Socket的概念和特点 1.1 Socket的概念
    的头像 发表于 11-12 14:22 262次阅读

    深入理解 Llama 3 的架构设计

    最新的自然语言处理(NLP)技术和深度学习算法,旨在提供更加自然、流畅和智能的对话体验。 1. 核心组件 Llama 3的架构设计可以分为以下几个核心组件: 1.1 预处理模块 预处理模块负责将原始文本数据转换为模型可以理解的格式。这包括文本清洗
    的头像 发表于 10-27 14:41 536次阅读

    深入理解FPD-link III ADAS解串器HUB产品

    电子发烧友网站提供《深入理解FPD-link III ADAS解串器HUB产品.pdf》资料免费下载
    发表于 09-06 09:58 1次下载
    <b class='flag-5'>深入理解</b>FPD-link III ADAS解串器HUB产品

    锡焊原理解析:深入理解电子产品制造的核心工艺

    探索焊接技术在精密电子工程中的重要性和创新,从基础元件的连接到现代焊接技术的进展,深入了解焊接材料的选择与焊接技术的分类。本文提供了对锡焊原理的深入分析,揭示了高质量电子产品制造的关键因素。
    的头像 发表于 08-12 15:03 668次阅读
    锡焊原<b class='flag-5'>理解</b>析:<b class='flag-5'>深入理解</b>电子产品制造的核心工艺

    深入理解FFmpeg阅读体验》

    一、编译X264 H.264是ITU(International Telecommunication Union,国际通信联盟)和MPEG(Motion Picture Experts Group,运动图像专家组)联合制定的视频编码标准。而X264是一个开源的H.264/MPEG-4 AVC视频编码函数库,是最好的有损视频编码器之一。 先直接从网络(http://download.videolan.org/pub/videolan/x264/snapshots/)获取X264源码。考虑到版本关系,本文我下载的是x264-snapshot-20180430-2245-stable.tar.bz2。 tar -vxf x264-snapshot-20180430-2245-stable.tar.bz2 mkdir x264 手动创建的X264文件夹用于存放编译后的X264库。执行如下命令: ./configure --host=aarch64-linux --prefix=/home/x264 --enable-shared --disable-asm --enable-static --cross-prefix=aarch64-linux-gnu- 之后执行make make install完成x264库的交叉编译。生成的文件信息如下: root@EliteDesk800:~/x264/lib$ ls libx264.solibx264.so.152pkgconfig root@EliteDesk800:~/x264/lib$ file * libx264.so:symbolic link to libx264.so.152 libx264.so.152: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=01dd733d65c98eb894b4cdd41216259543ec8405, with debug_info, not stripped pkgconfig:directory 二、编译FFmpeg 首先从FFmpeg官方网站http://ffmpeg.org/download.html上下载FFmpeg源码。 tar -vxf ffmpeg-snapshot.tar.bz2 mkdir ffmpeg_install 其中ffmpeg_install文件夹用于保存生成的文件。执行如下命令: ./configure --prefix=/home/ffmpeg_install --enable-cross-compile --arch=arm64 --target-os=linux --cc=aarch64-linux-gnu-gcc --disable-x86asm --cross-prefix=aarch64-linugnu---pkg-config=/usr/bin/pkg-config --pkg-config=/usr/bin/pkg-config主要用于解决ERROR: x264 not found using pkg-config问题,网上很多解决方法都不靠谱。 之后执行make make install完成ffmpeg的交叉编译。 生成的文件信息如下: root@EliteDesk800:~/ffmpeg$ file ../ffmpeg_install/bin/* ../ffmpeg_install/bin/ffmpeg:ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=b32285f11866f79dd499330849a9b3195ea0e446, stripped ../ffmpeg_install/bin/ffprobe: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=5d3fa4ea21ad6c4395bbaf134b915190799305b2, stripped
    发表于 04-16 22:54

    深入理解 Sora 的技术原理

    将去除噪音后的结果数据,利用视频解码器进行解码,将低维潜在空间数据还原成原始视频数据,这里可以实现不同分辨率的视频解码。
    的头像 发表于 04-05 09:19 1939次阅读
    <b class='flag-5'>深入理解</b> Sora 的技术原理

    深入理解 FPGA 的基础结构

    转载地址:https://zhuanlan.zhihu.com/p/506828648 文章很详细的介绍了FPGA的基础结构,能更直观的理解内部结构原理。对深入学习很有帮助。 以下是正文: 这一段
    发表于 04-03 17:39

    深入理解数据备份的关键原则:应用一致性与崩溃一致性的区别

    深入理解数据备份的关键原则:应用一致性与崩溃一致性的区别 在数字化时代,数据备份成为了企业信息安全的核心环节。但在备份过程中,两个关键概念——应用一致性和崩溃一致性,常常被误解或混淆。本文旨在阐明
    的头像 发表于 03-11 11:29 893次阅读
    <b class='flag-5'>深入理解</b>数据备份的关键原则:应用一致性与崩溃一致性的区别

    深入理解GPIO原理和运用

    GPIO允许我们的单片机与外部世界进行通信,它是我们控制外部设备和接收外部信号的接口。
    的头像 发表于 02-19 16:12 1888次阅读
    <b class='flag-5'>深入理解</b>GPIO原理和运用

    恒讯科技带大家深入理解:WebSocket服务器的工作原理

    WebSocket是一种在单个TCP连接上进行全双工通信的通信协议。它的设计目标是在Web浏览器和服务器之间提供低延迟、高效的双向通信。下面是深入理解WebSocket服务器工作原理的一些关键概念
    的头像 发表于 01-29 16:48 465次阅读

    深入理解FFmpeg阅读体验》ffmpeg安装

    书中讲了如何编译安装,我看了一下比较复杂,所以下载安装包进行安装,下载网址:Download FFmpeg 下载完后解压出来在bin目录下面有三个文件: 复制这个目录: C:\\\\Users\\\\liujianhua\\\\Downloads\\\\ffmpeg-6.1.1-essentials_build\\\\ffmpeg-6.1.1-essentials_build\\\\bin 打开环境变量: 将刚才复制的地址添加进去 到此就安装结束,测试一下是否安装成功,打开shell输入ffmpeg -version就可以看到输出信息:
    发表于 01-25 21:35

    深入理解光耦模拟隔离放大电路的技术奥秘

    深入理解光耦模拟隔离放大电路的技术奥秘 ​编辑 ▲ 图1 仿真原理图二、原理分析 之所以这个电路图看起来容易让人感到困惑,实际上就是这个仿真电路中,错误的使用了这样的光电三极管来表示HCNR201
    发表于 01-10 10:12

    深入理解FFmpeg阅读体验》初识有感

    言中《如何阅读本书》指出我们如何开始学习FFmpeg。老师建议从头开始,上篇为基础篇,下篇为API使用以及开发。 第三,书中还给我们指出如何从FFmpeg官方文档、wiki以及推荐雷霄骅博士的学习资料。 经过阅读前面几章,让我感觉入门FFmpeg应该学习资料非常之多,只要认真的学习,应该可以掌握这门技术。
    发表于 01-07 19:48

    深入理解FFmpeg阅读体验》+ 书收到了,崭新的开篇

    今天收到了《深入理解FFmpeg》 崭新的书,一个在2022年较近距离接触过却尚未深入研究的领域图像处理。最近刚好在作这方面的研究,希望自己可以把握这次机会,好好学习下 FFMpeg,相信可以让自己
    发表于 01-07 18:57