web应用模式、API接口、接口测试工具postman、如何在浏览器中测试、restful规范、序列化反序列化、基于Django原生编写五个接口、drf介绍和快速使用、drf之APIView源码分析

一、web应用模式

  • Django框架就是一种web框架,专门用来写web项目,之前学的,写的BBS项目,图书管理系统,用的都是前后端混合开发
	-后端人员,写后端,也要写【模板语法】---》xx.html的python代码
    -全栈开发-->前后端混合时代,比较多
  • 从今天开始,学的是前后端分离
	-后端人员,一点前端都不需要动,只需要写接口即可
    -全栈开发---》web后端,前端框架(vue,react)

在开发Web应用中,有两种应用模式:
1.前后端不分离客户端看到的内容和所有界面效果都是由服务端提供出来的

image

image

2.前后端分离【把前端的界面效果(htmlcssjs分离到另一个服务端,python服务端只需要返回数据即可)】

前端形成一个独立的网站,服务端构成一个独立的网站

image

image

二、API接口

为了在团队内部形成共识、防止个人习惯差异引起的混乱,我们需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少双方之间的合作成本【前后端合作】
API概念
API:应用程序接口(API:Application Program Interface

应用程序接口(API:application programming interface)是一组定义、程序及协议的集合,通过 API 接口实现计算机软件之间的相互通信。API 的一个主要功能是提供通用功能集。程序员通过使用 API 函数开发应用程序,从而可以避免编写无用程序,以减轻编程任务
简单来说就是通过网络,规定了前后端信息交互规定的URL链接,也就是前后端信息交互的媒介

    http://127.0.0.1/books/   
	点赞,点踩  就是API接口
  • URL:长得像返回数据的URL链接
    https://api.map.baidu.com/place/v2/search

  • 请求方式:getpostputpatchdelete
    采用get方式请求上方接口

  • 请求参数:jsonxml格式的key-value类型数据
    ak6E823f587c95f0148c19993539b99295
    region:上海
    query:肯德基
    output:json

	-早些年 前后端交互使用xml格式----》ajax:异步JavaScript和XML
	-后来,随着json格式的出现,成了主流,直到现在
	-以后:一定会出现,比json更高效的交互格式,更安全
  • 响应结果:json或xml格式的数据
    • 上方请求参数的output参数值决定了响应数据的格式
    • 数据
# xml格式
https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295&region=%E4%B8%8A%E6%B5%B7&query=%E8%82%AF%E5%BE%B7%E5%9F%BA&output=xml
'''中文会变成上面的样式,上下两个url是一样的意思'''
https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295&region=上海&query=肯德基&output=xml
#json格式
https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295&region=%E4%B8%8A%E6%B5%B7&query=%E8%82%AF%E5%BE%B7%E5%9F%BA&output=json

https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295&region=上海&query=肯德基&output=json

'''下方是返回的值'''
{
    "status":0,
  	"message":"ok",
    "results":[
        {
            "name":"肯德基(罗餐厅)",
            "location":{
                "lat":31.415354,
                "lng":121.357339
            },
            "address":"月罗路2380号",
            "province":"上海市",
            "city":"上海市",
            "area":"宝山区",
            "street_id":"339ed41ae1d6dc320a5cb37c",
            "telephone":"(021)56761006",
            "detail":1,
            "uid":"339ed41ae1d6dc320a5cb37c"
        }
      	...
		]
}

三、接口测试工具postman

postman介绍

作为后端,写好了接口,我们自己要测试通过,再给别人用。

而后端测试人员需要测试API接口的时候,总不可能在浏览器里面试用吧,所以有了专业的接口测试工具Postman

	-浏览器只能发送get请求,不能自动发送post,delete请求
    -postman---》开源软件,只是谷歌浏览器的插件,越做好好,后来可以按装到操作系统上,再后来,收费
    -postwoman
    -很多很多其他的,不同公司用的也可能不一样,你只需要明白一个点,这个工具只是用来发送http请求

postman下载与使用

1、官方下载
https://www.postman.com/downloads/?utm_source=postman-home----》Postman-win64-Setup.exe
2、安装时,双击即可,马上就会出现下图界面
image
3、使用讲解

打开一个新的测试页面后界面如下,返回数据的窗口在没有发送请求之前是空的。
image
点击界面中间的Body,可以选择发送请求的方式,需要注意在使用form-data请求方式,即用form表单传输文件的时候,需要手动选择传输的数据类型
image

image

这里返回的结果
image

四、如何在浏览器中测试

我们需要再settings.py文件中注册rest_framework才能在浏览器中进行接口测试

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
    'rest_framework',
]

五、restful规范(重要)

概念
REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征性状态转移)。 它首次出现在2000年Roy Fielding的博士论文中

RESTful是一种定义Web API接口的设计风格,尤其适用于前后端分离的应用模式中
这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源

事实上,我们可以使用任何一个框架都可以实现符合restful规范的API接口

十条规范

   -1 数据的安全保障,通常使用https(http+ssl/tsl)协议
        -url链接一般都采用https协议进行传输
        -采用https协议,可以提高数据交互过程中的安全性
        
   -2 接口中带api标识
		-https://api.lqz.com/books(可以写在域名中)
        -https://www.lqz.com/api/books(也可以写在路由中)    咱们用这个
            
   -3 多版本共存,路径中带版本信息
		-https://api.lqz.com/v1/login
        -https://api.lqz.com/v2/login
		-https://www.lqz.com/api/v1/login
        -https://www.lqz.com/api/v2/login
            
   -4 数据即是资源,均使用名词,尽量不出现动词(最核心的)
		-接口一般都是用于完成前后台数据的交互,交互的数据我们称之为资源
    	-接口形式如下
            https://api.baidu.com/users
            https://api.baidu.com/books
        -特殊的接口可以出现动词,因为这些接口一般没有一个明确的资源,或是动词就是接口的核心含义、				https://api.baidu.com/login
        
        
   -5 资源操作由请求方式决定(method)
		-操作资源一般都会涉及到增删改查,我们提供请求方式来标识增删改查动作
        https://api.baidu.com/books    - get请求:获取所有书(获取所有)
        https://api.baidu.com/books/1  - get请求:获取主键为1的书(获取一条)
        https://api.baidu.com/books    - post请求:新增一本书书(新增一条)
        https://api.baidu.com/books/1  - put请求:修改主键为1的书(整体修改一条)
        https://api.baidu.com/books/1  - patch请求:修改主键为1的书(局部修改一条)
        'ps:patch用的不多,一般用put即可'
        https://api.baidu.com/books/1  - delete请求:删除主键为1的书(删除一条)
            
            
   -6 在请求地址中带过滤条件
    https://api.example.com/v1/zoos?limit=10:指定返回记录的数量
    https://api.example.com/v1/zoos?offset=10:指定返回记录的开始位置
    https://api.example.com/v1/zoos?page=2&per_page=100:指定第几页,以及每页的记录数
    https://api.example.com/v1/zoos?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序
    https://api.example.com/v1/zoos?animal_type_id=1:指定筛选条件
    
    
   -7 响应中状态码:两套
		-http响应状态码:1xx:请求正在处理,2xx:成功响应,3xx:重定向,4xx:客户端错误,5xx:服务端错误
    '常见的需要记住'
    	响应状态码2XX		200:常规请求		201:创建成功
	 	响应状态码3XX		301:永久重定向	302:暂时重定向
	 	响应状态码4XX		403:请求无权限	404:请求路径不存在	405:请求方法不存在
	 	响应状态码5XX		500:服务器异常
    		-https://blog.csdn.net/li_chunlong/article/details/120787872
        -公司内部规定的响应状态码,放在响应体中
        	{code:0}   咱们后期一般使用100  101 102这种
            
            
    -8 返回数据中带错误信息
    	{
            code:0
    		msg: "ok/用户名错误"
		}
        
        
    -9 返回的结果应该符合以下规范---》好多公司不遵循这个
        GET 获取所有数据:返回资源对象的列表(数组)[{name:红楼梦,price:99},{name:红楼梦,price:99},{name:红楼梦,price:99}]
        GET 单个对象:返回单个资源对象:{name:红楼梦,price:99}
        POST 新增对象:返回新生成的资源对象:{name:西游记,price:99}
        PUT 修改对象:返回完整的资源对象 :{name:西游记,price:100}
        DELETE 删除:返回一个空文档 
        
        
    -10 响应数据中带连接
    		# Hypermedia API,RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么
		{
		  	"status": 0,
		  	"msg": "ok",
		  	"results":[
		        {
		            "name":"肯德基(罗餐厅)",
		            "img": "https://image.baidu.com/kfc/001.png"
		        }
		      	...
				]
		}
	
	比较好的接口返回
		# 响应数据要有状态码、状态信息以及数据本身
		{
		  	"status": 0,
		  	"msg": "ok",
		  	"results":[
		        {
		            "name":"肯德基(罗餐厅)",
		            "location":{
		                "lat":31.415354,
		                "lng":121.357339
		            },
		            "address":"月罗路2380号",
		            "province":"上海市",
		            "city":"上海市",
		            "area":"宝山区",
		            "street_id":"339ed41ae1d6dc320a5cb37c",
		            "telephone":"(021)56761006",
		            "detail":1,
		            "uid":"339ed41ae1d6dc320a5cb37c"
		        }
		      	...
				]
		}

六、序列化反序列化

api接口开发,最核心最常见的一个过程就是序列化,所谓序列化就是把【数据转换格式】,序列化可以分两个阶段:

序列化: 把我们识别的数据转换成指定的格式提供给别人

-字典,列表------》json格式存到文件中了
	-例如:我们在django中获取到的数据默认是模型对象,但是模型对象数据无法直接提供给前端或别的平台使用,所以我们需要把数据进行序列化,变成字符串或者json数据,提供给别人
	-read

反序列化:把别人提供的数据转换/还原成我们需要的格式

-例如:前端js提供过来的json数据,对于python而言就是字符串,我们需要进行反序列化换成模型类对象,这样我们才能把数据保存到数据库中。
    -write

七、基于Django原生编写五个接口

# 以后写的接口,基本上都是五个接口及其变形
	-查询所有
	-查询单个
	-新增一个
	-修改一个
	-删除一个

# 基于books单表为例,写五个接口
	-创建book表
	-表迁移
	-录入假数据:直接录,后台管理录
	-写查询所有接口----》遵循restful规范,使用cbv
	-新增一条数据-----》
	-查询一条
	-修改一条:put提交的数据,不能从request.POST中取
	-删除一条

models.py

form django.db import models

class Book(models.Model):
	name = models.CharField(max_length=32)
	price = models.CharField(max_length=32)
	publish = models.CharField(max_length=32)

urls.py

urlpatterns = [
	path('admin/', admin.site.urls),
	path('api/v1/books/', views.BookView.as_view()),
	path('api/v1/books/<int:pk>/', views.BookDetailView.as_view()),
]

views.py

from django.shortcuts import render,HttpResponse,redirect

from django.http import JsonResponse
from .models import Book	# 相对导入
from django.views import View

import json

class BookView(View):
	# 查询所有
	def get(self, request):
		# 查询出所有的图书,queryset对象,不能直接给前端
		books = Book.objects.all()
		# 转成json格式,给前端
		# 把quer对象转成列表,然后再使用JsonResponse
		book_list = []
		for book in books:
			book_list.append({'name': book.name, 'price': book.price, 'publish': book.publish})
		# 扩展做了解
		# return HtpResponse(json.dumps(book_list, ensure_ascii=False))	# 指定ensure_ascii为false,前端就显示中文了
		return JsonResponse(book_list, safe=False, json_dumps_params={'ensure_ascii': False})	# JsonRsponse只能放字典或列表
		
	# 薪增一个(只能使用urlencoded或form-data编码,使用json形式编码不行,因为json格式编码提交的数据,不能从request.POST中取,从body中)
	def post(self, request):
		# 取出前端传入的数据
		name = request.POST.get('name')
		price = reqeust.POST.get('price')
		publish = request.POST.get('publish')
		# 存到数据库中
		book = Book.objects.create(name=name, price=price, publish=publish)
		# 返回新增的对象字典
		return JsonResponse({'name': book.name, 'price': book.price, 'publish': book.publish})

class BookDetailView(View):
	def get(self, request, pk):
		book = Book.objectsfilter(pk=pk).first()
		# book模型对象转成字典,使用JsonResponse返回
		return JsonResponse({'id': book.id, 'name': book.name, 'price': book.price, 'publish': book.publish})
	
	def put(self, request, pk):	# request.POST只能取post提交的urlencoded或form-data编码数据,put提交的取不到
		# 查到要改的
		book = Book.objects.filter(pk=pk).first()
		# 取出前端传入的数据,修改完,保存----》存在问题,因为put提交的取不到
		# book.name = request.POST.get('name')
		# book.price = request.POST.get('price')
		# book.publish = request.POST.get('publish')
		# book.save()
		
		# 前端使用json格式提交,自己保存
		print(request.body)
		book_dict=json.loads(request.body)
		book.name = book_dict.get('name')
		book.price = book_dict.get('price')
		book_publish = book_dict.get('publish')
		book.save()
		
		return JsonResponse({'id': book.id, 'name': book.name, 'price': book.price, 'publish': book.publish})
		
	def delete(self, request, pk):
		Book.objects.filter(pk=pk).delete()
		return JsonResponse(data={})

八、drf介绍和快速使用

概念

核心思想: 缩减编写api接口的代码

Django REST framework是一个建立在Django基础之上的Web 应用开发框架,可以快速的开发REST API接口应用。在REST framework中,提供了序列化器Serialzier的定义,可以帮助我们简化序列化与反序列化的过程,不仅如此,还提供丰富的类视图、扩展类、视图集来简化视图的编写工作。REST framework还提供了认证、权限、限流、过滤、分页、接口文档等功能支持。REST framework提供了一个API 的Web可视化界面来方便查看测试接口。

官方文档:https://www.django-rest-framework.org/

github: https://github.com/encode/django-rest-framework/tree/master

特点(了解一下)

  • 提供了定义序列化器Serializer的方法,可以快速根据 Django ORM 或者其它库自动序列化/反序列化;
  • 提供了丰富的类视图、Mixin扩展类,简化视图的编写;
  • 丰富的定制层级:函数视图、类视图、视图集合到自动生成 API,满足各种需要;
  • 多种身份认证和权限认证方式的支持;[jwt]
  • 内置了限流系统;
  • 直观的 API web 界面;
  • 可扩展性,插件丰富

安装

# djangorestframework:  drf 帮助我们快速的实现符合restful规范的接口

# django 最新 4.x ,一般都会用最新版的上一版3.x
# drf最新支持到djagno 3.x ,最新不支持2.x
# 安装drf
	pip3 install djangorestframework -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com/simple/
    # 由于你是django2.x 它发现它不支持,它会自动写在dajgno,安装最新的django 4.x

使用drf编写五个接口

注意:url的末尾必须要写斜杠符号,不然会报错
urls.py

from app01.views import BookView
from rest_framework.routers import SimpleRouter
router = SimpleRouter
router = SimpleRouter()
router.register('books', BookView, 'books')
urlpatterns = [
	path('admin/', admin.site.urls),
]
# 两个列表相加 [1,2,4] + [6,7,8]=
urlpatterns += router.urls

views.py

form .serializer import BookSerializer
from rest_framework.viewsets import ModelViewSet
class BookView(ModelViewSet):
	queryset = Book.objects.all()
	serializer_class = BookSerializer

serializer.py

from rest_framework import serializers
form .models import Book
class BookSerializer(serializers.ModelSerializer):
	class Meta:
	model = Book
	fields = '__all__'

九、drf之APIView源码分析

基于APIView写五个接口

urls.py

urlpatterns = [
	path('books/', BookView.as_view()),
	path('books/<int:pk>/', BookDetailView.as_view()),
]

views.py

form rest_framework.views import APIView	# APIView继承了Django原来的View
from .serializer import BookSerializer
from rest_framework.response import Response

class BookView(APIView):
	# 查询所有
	def get(self, request):
		book_list = Book.objects.all()
		# drf提供了序列化类
		ser = BookSerializer(instance=book_list, many=True)	# 序列化
		return Response({'code': 100, 'msg': '成功', 'result': ser.data})
	
	def post(self, request):
		ser = BookSerializer(data=request.data)	# 反序列化
		if ser.is_valid():	# 数据校验---》有些不合法禁止
			ser.save()	# 保存到数据库中
		return Response({'code': 100, 'msg': '成功'})

class BookDetailView(APIView):
	# # 查询单条
	def get(self, request, pk):
		book = Book.objects.filter(pk=pk).first()
		ser = BookSerializer(instance=book,many=False)	# 序列化
		return Response({'code': 100, 'msg': '成功', 'result': ser.data})
	# 修改一条
	def put(self, request, pk):
		book = Book.objects.filter(pk=pk).first()
		ser = BookSerializer(instance=book,data=request.data)	# 反序列化
		if ser.is_valid():	# 数据校验---》有些不合法禁止
			ser.save()	# 保存到数据库中
		return Response({'code': 100, 'msg': '成功'})
	def delete(self, request, pk):
		Book.objects.filter(pk=pk).delete()
		return Response({'code': 100, 'msg': '删除成功'})

serializer.py

from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
	class Meta:
		model = Book
		fields = '__all__'

CBV源码分析

# cbv写法:
	1 视图中写视图类,继承View,写跟请求方式同名的方法
    	class BookView(View):
            def get(self,request):
                return 四件套
     2 在路径用写
    	path('books/', BookView.as_view())
        
        
        
# 如上写法,为什么能够执行

# 前置条件:前端请求,一旦路径匹配成功,就会执行  BookView.as_view()(request传入,)
# 入口在  BookView.as_view()--->执行结果---》View中有个as_view类的绑定方法
    @classmethod
    def as_view(cls, **initkwargs):
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            res=self.dispatch(request, *args, **kwargs)
            return res
        return view
    
# 执行结果是view 的内存地址: 请求来了,执行view(request)
	path('books/', view)
    
# 执行 View类中的as_view方法中的内层的view函数,路由匹配成功,本质是在执行
	self.dispatch(request, *args, **kwargs)
    # self是谁的对象?BookView的对象
    # 去BookView中dispatch,找不到,去父类,View中找到了
    
    
# View这个类的dispatch
    def dispatch(self, request, *args, **kwargs):
        # request.method.lower() 如果是get请求,  ‘get’ 在这个列表里面
        if request.method.lower() in self.http_method_names:
            # handler=getattr(BookView的对象,'get')   
            # handler就是BookView类中的get方法
            handler = getattr(self, request.method.lower())
        else:
            handler = self.http_method_not_allowed
        # 执行 BookView类中的get方法 (request)
        return handler(request, *args, **kwargs)
    
# 最终本质跟写fbv的执行流程一样
# 最终结论:什么请求方式,就会执行视图类中的什么方法
	-本身请求来了,匹配成功,会执行view(request)
	def view(request, *args, **kwargs):
		return self.dispatch(request, *args, **kwargs)
	-self.dispatch  View类的方法
	def dispatch(self, request, *args, **kwargs):
		# request.method请求方式转成小写,必须在列表中才能往下走
		if request.method.lower() in self.http_method_names:
			# 反射,去self【视图类的对象:BookView】,去通过get字符串,反射出属性或方法
			# BookView的get方法
			handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
		else:
			handler = self.http_method_not_allowed
		# BookView的get方法,加括号,传入request
		return hander(request, *args, **kwargs)

APIView执行流程-源码分析

urls.py

path('books/', views.BookView.as_view()),---》请求来了,执行views.BookView.as_view()()--->现在as_view是APIView的as_view(因为我们把父类从View换成了APIView)

我们点进源码查看可以发现APIView其实继承了View

class APIView(View):

往下查找as_view()的代码

# APIView的as_view方法:view还是原来的view,但是以后再也没有csrf认证了
	@classmethod
	def as_view(cls, **initkwargs):
		# 调用父类的as_view,父类是Django原生view
		# 把Django原生view的as_view方法中的闭包函数view拿出来了
		view = super().as_view(**initkwargs)
		# csrf_exempt 排除所有csrf的认证
		# 相当于再所有的方法上面加了这个装饰器
		return csrf_exempt(view)
'从他内部的view我们可以看到他是用派生方法调用父类View中的as_view因此我们说view还是原来的view,但是返回的结果是csrf_exempt(view),回顾装饰器的知识,我们可以知道虽然把装饰器写成了语法糖,但是在运行的时候其实就是把被装饰的函数名称传进来当参数运行,因此这里就是取消了csrf认证的作用'
# 路由匹配成功,执行的其实就是csrf_exempt(view)(request)--->而这个view就是View的as_view中的闭包函数view---》然后执行self.dispatch--->self是视图类的对象---》BookView产生的对象---》这时候会执行self.dispatch,根据名称的查找顺序他会查找到APIView的dispatch:
	def dispatch(self, request, *args, **kwargs):
		# 这里的参数中的request是Django原生request,老的request
		# 把老的request包装成了新的request,这个是drf提供的Request类的对象
		request = self.initialize_request(request, *args, **kwargs):
		# 到此以后,这个request就是新的了,老的request在request._request这是老的
		
		# 把新的request放到了self对象【BookView的对象】
		self.request = reqeust
		try:
			# 执行了三大认证【认证、频率、权限】,使用新的request,之后会将
			self.initial(request, *args, **kwargs)
			# 跟之前一模一样用反射获取执行结果,如果报错就用异常捕获处理
			if request.method.lower() in self.http_method_names:
				handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
			else:
				handler = self.http_method_not_allowed
					# 把新的request传入了,视图类的方法中get的request也是新的
			response = handler(request, *atgs, **kwargs)
		except Exception as exc:
			# 在执行3大认证和视图类中方法的过程中,如果出了异常,都能捕获到---》全局异常捕获
			response = self.handle_exception(exc)
		self.response = self.finalize_response(request, response, *args, **kwargs)
		return self.response
		
		

'上方的request调用了initialize_request方法,这个方法的返回结果产生了一个Request生成的对象,它的源码如下:'
	def initialize_request(self, request, *args, **kwargs):
		parser_context = self.get_parser_context(request)
		return Request(
			reqeust,
			parsers=self.get_parsers(),
			authenticators=self.get_authenticators(),
			negotiator=self.get_content_negotiator(),
			parser_context=parser_context
	)
'接着我们点进这个Request类的源码,我们会发现老的request变成了_request属性'
	def __init__(self, request, parsers=None, authenticators=None, negotiator=None, parser_context=None):
		self.request = request
		self.parsers = parsers or ()
		self.authenticators = authenticators or ()
		self.negotiator = negotiator or self._default_negotiator()
		self.parser_context = parser_context
		self._data = Empty
		self._files = Empty
		self._full_data = Empty
		self._content_type = Empty
		self._stream = Empty
		
		
# 总结:APIView的执行流程
1.去除了所以的csrf
2.包装了新的request,以后在视图类中用的request Request类的对象,不是原生的了
	-原生的在:新的request._request
3.在执行视图类的方法之前,执行了三大认证
4.如果三大认证或视图函数方法执行过程中出了错,会有异常捕获---》全局异常捕获
5.以后视图类方法中的request都是新的



# 补充:装饰器的基本原理
def auth()	# 装饰器
def add()	# 函数

# 使用auth装饰add函数
@auth	# 本质是	add=auth(add)
def add()

# 以后再使用add,其实就是在使用 auth(add) 的返回结果
posted @ 2023-05-17 14:13  吴仁耀  阅读(146)  评论(0编辑  收藏  举报