当前位置: 首页 > news >正文

python后端之DRF框架(下篇)

四、视图集和路由

4.1、GenericAPIView

rest_framework.generics.GenericAPIView 继承自 APIVIew ,增加了对于列表视图和详情视图可能用
到的通用支持方法。

4.1.1、扩展的类属性:

queryset: 指定当前类视图使用的查询集
serializer_class :类视图使用的序列化器

4.1.2、扩展的方法:

self.queryset():获取查询集
self.serializer():获取序列化器
self.get_object():获取指定的单一对象

4.1.3、扩展功能:

pagination_class :数据分页
filter_backends:数据过滤&排序
指定单一数据获取的参数字段:lookup_field 查询单一数据库对象时使用的条件字段,默认为' pk 'lookup_url_kwarg 查询单一数据时URL中的参数关键字名称,默认与look_field相同

4.2、视图扩展类

4.2.1、基本扩展类

ListModelMixin:
列表视图扩展类,提供`list方法快速实现列表视图。
返回200状态码。
CreateModelMixin :
创建视图扩展类,提供create方法快速实现创建资源的视图。
成功返回201状态码,如果序列化器对前端发送的数据验证失败,返回400错误。
RetrieveModelMixin:获取单一数据
详情视图扩展类,提供retrieve方法,可以快速实现返回一个存在的数据对象。
如果成功,返回200, 否则返回404。
UpdateModelMixin:更新数据
更新视图扩展类,提供update方法和partial_update方法,可以快速实现更新一个存在的
数据对象。
成功返回200,序列化器校验数据失败时,返回400错误。
DestroyModelMixin:
删除视图扩展类,提供destroy方法,可以快速实现删除一个存在的数据对象。
成功返回204,不存在返回404。

4.2.2 、视图扩展类

1、CreateAPIView
继承自: GenericAPIView、CreateModelMixin
提供 post 方法
2、ListAPIView
继承自:GenericAPIView、ListModelMixin
提供 get 方法
3、RetireveAPIView
继承自: GenericAPIView、RetrieveModelMixin
提供 get 方法
4、DestoryAPIView
继承自:GenericAPIView、DestoryModelMixin
提供 delete 方法
5、UpdateAPIView
继承自:GenericAPIView、UpdateModelMixin
提供 put 和 patch 方法
6、RetrieveUpdateAPIView
继承自: GenericAPIView、RetrieveModelMixin、UpdateModelMixin
提供 get、put、patch方法
7、RetrieveUpdateDestoryAPIView
继承自:GenericAPIView、RetrieveModelMixin、UpdateModelMixin、
DestoryModelMixin
提供 get、put、patch、delete方法

4.2.3、视图集

1、视图集的使用
ViewSet视图集类不再实现get()、post()等方法,而是实现动作 action 如 list() 、create() 等。将一系列逻辑相关的动作放到一个类中:

list() 提供一组数据
retrieve() 提供单个数据
create() 创建数据
update() 保存数据
destory() 删除数据

2、action属性
视图集只在使用as_view()方法的时候,才会将action动作与具体请求方式对应上。
3、常用视图集类

1) ViewSet
继承自 APIView ,作用也与APIView基本类似,提供了身份认证、权限校验、流量管理等。
在ViewSet中,没有提供任何动作action方法,需要我们自己实现action方法。
2)GenericViewSet
继承自 GenericAPIView ,作用也与GenericAPIVIew类似,提供了get_object、get_queryset等方法便于列表视图与详情信息视图的开发。
3)ModelViewSet
继承自 GenericAPIVIew ,同时包括了ListModelMixin、RetrieveModelMixin、CreateModelMixin、
UpdateModelMixin、DestoryModelMixin。
4)ReadOnlyModelViewSet
继承自 GenericAPIVIew ,同时包括了ListModelMixin、RetrieveModelMixin。

4、路由
对于视图集ViewSet,我们除了可以自己手动指明请求方式与动作action之间的对应关系外,还可以使用Router来帮助我们快速实现路由信息。
REST framework提供了两个Router:

SimpleRouter(推荐)
DefaultRouter(不推荐)
DefaultRouter与SimpleRouter的区别是,DefaultRouter会多附带一个默认的API根视图,返
回一个包含所有列表视图

(1)、 创建router对象并注册

from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'vips', BookInfoViewSet)
register(prefix, viewset, base_name)prefix 该视图集的路由前缀viewset 视图集base_name 路由名称的前缀

如上述代码会形成的路由如下:

^vips/$
^vip/{pk}/$

(2)、添加路由数据

urlpatterns = [
...
]
urlpatterns += router.urls

五、其他功能

1、认证&权限

1、认证

1)、 settings.py 全局配置
在配置文件中配置全局默认的认证方案

REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework.authentication.BasicAuthentication', # Basic认证'rest_framework.authentication.SessionAuthentication', # session认证)
}

2)、单个视图配置
在视图中通过设置authentication_classess属性来设置视图的认证方案

from rest_framework.authentication import SessionAuthentication,BasicAuthentication
from rest_framework.views import APIView
class VIPView(APIView):# 指定认证的方式authentication_classes = (SessionAuthentication, BasicAuthentication)

认证失败会有两种可能的返回值:401 Unauthorized 未认证、403 Permission Denied 权限被禁止

2、权限

权限控制可以限制用户对于视图的访问和对于具体数据对象的访问。
在执行视图的dispatch()方法前,会先进行视图访问权限的判断。
在通过get_object()获取具体对象时,会进行对象访问权限的判断。
1)、全局权限管理
在配置文件中设置默认的权限管理类

REST_FRAMEWORK = {'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',)
}

如果未指明,默认采用如下默认配置(所有用户均可访问)

'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',
)

2)、单个视图权限
在视图中通过permission_classes属性来设置权限

from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class VIPView(APIView):permission_classes = (IsAuthenticated,)

3)、权限选项

AllowAny 允许所有用户
IsAuthenticated 仅通过认证(登录)的用户
IsAdminUser 仅管理员用户
IsAuthenticatedOrReadOnly 认证的用户可以完全操作,否则只能get读取

2、限流

对接口访问的频次进行限制,以减轻服务器压力(反爬虫的一种手段)。
1、限流类型

AnonRateThrottle限制所有匿名未认证用户,使用IP区分用户。使用DEFAULT_THROTTLE_RATES['anon'] 来设置频次
UserRateThrottle限制认证用户,使用User id 来区分。使用DEFAULT_THROTTLE_RATES['user']来设置频次
ScopedRateThrottle限制用户对于具体视图的访问频次,通过ip或user id。视图中使用throttle_scope 指定频次

2、全局配置
DEFAULT_THROTTLE_CLASSES:设置限流类型
DEFAULT_THROTTLE_RATES:设置限制的频次

REST_FRAMEWORK = {# 限流规则'DEFAULT_THROTTLE_RATES': {'anon': '10/minute', # 未认证的用户,每分钟10次'user': '10000/minute' # 认证的用户,每分钟10000次}# 限流策略'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.AnonRateThrottle','rest_framework.throttling.UserRateThrottle'],
}

频率周期

second:每秒
minute:每分钟
hour:每小时
day:每天

3、局部配置
在具体视图中通过throttle_classess属性来指定限流的类型

from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
class ExampleView(APIView):# 类视图中指定限流类型throttle_classes = (UserRateThrottle,)

3、过滤

对于列表数据可能需要根据字段进行过滤,我们可以通过添加django-fitlter扩展来增强支持。

pip install django-filter

1、全局配置

在配置文件中增加过滤后端的设置:

# 注册应用,
INSTALLED_APPS = [...'django_filters',
]
# 指定过滤器
REST_FRAMEWORK = {'DEFAULT_FILTER_BACKENDS':('django_filters.rest_framework.DjangoFilterBackend',)
}

在视图中添加filterset_fields属性,指定可以过滤的字段

class StudentView(ListAPIView):queryset = BookInfo.objects.all()serializer_class = BookInfoSerializerfilterset_fields = ('age',)
# 127.0.0.1:8000/students/?age=18

4、排序

对于列表数据,REST framework提供了OrderingFilter过滤器来帮助我们快速指明数据按照指定字段进行排序。
1、全局配置
(1)在setting中的 REST_FRAMEWORK 添加配置

'DEFAULT_FILTER_BACKENDS': (
# 这个是指定使用django_filters中的过滤器来进行过滤
'django_filters.rest_framework.DjangoFilterBackend',
# 这个是指定使用DRF自带的排序过滤器来进行数据排序
'rest_framework.filters.OrderingFilter'
),

(2)在视图类中指定排序可选字段:ordering_fields:
REST framework会在请求的查询字符串参数中检查是否包含了ordering参数,如果包含了
ordering参数,则按照ordering参数指明的排序字段对数据集进行排序。

class StudentView(ListAPIView):queryset = Student.objects.all()serializer_class = StudentSerializerordering_fields = ('age', 'id' ) # 指定排序的字段
# url 指明通过age字段排序
# 127.0.0.1:8000/students/?ordering=age
# url 指明通过id字段排序
# 127.0.0.1:8000/students/?ordering=id

注意:默认升序排序,降序排序字段前添加负号, ‘-’

5、分页

REST framework提供了分页的支持
1、全局配置
在配置文件中设置全局的分页方式:

REST_FRAMEWORK = {'DEFAULT_PAGINATION_CLASS':'rest_framework.pagination.PageNumberPagination','PAGE_SIZE': 10 # 每页数据量
}

2、局部配置
在不同的视图中可以通过pagination_class属性来指定不同的分页器。
自定义分页器:
定义一个继承PageNumberPagination的类型,在子类中通过属性定义分页器的数据:

page_size 每页默认的数据条数
page_query_param 前端发送的页数关键字名,默认为"page"
page_size_query_param 前端发送的每页数目关键字名,默认为None
max_page_size 每页最多的数据条数
class StuPagination(PageNumberPagination):# 默认每页数据量page_size = 20page_size_query_param = 'page_size'# 每页的数据量的最大值max_page_size = 10000

使用分页器:

class StuView(RetrieveAPIView):queryset = Students.objects.all()serializer_class = StudentsSerializerpagination_class = StuPagination

关闭分页功能:
如果在视图内关闭分页功能,只需在视图内设置

pagination_class = None

3、分页器类型
1) PageNumberPagination(常用)
前端访问网址形式:

http://127.0.0.1:8000/students/?page=4

子类中定义的属性:

page_size 每页数目
page_query_param 前端发送的页数关键字名,默认为"page"
page_size_query_param 前端发送的每页数目关键字名,默认为None
max_page_size 前端最多能设置的每页数量

2)LimitOffsetPagination
前端访问网址形式:

http://127.0.0.1:8000/students/?limit=100&offset=400

可以在子类中定义的属性:

default_limit 默认限制,默认值与PAGE_SIZE设置一直
limit_query_param limit参数名,默认'limit'
offset_query_param offset参数名,默认'offset'
max_limit 最大limit限制,默认None

6、异常处理

REST framework提供了异常处理,如果没有自定义默认会采用默认的处理方法方式

#写不写都是这个配置
REST_FRAMEWORK = {
# REST framework中默认的异常处理方法
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}

6.1、自定义异常处理的方法

1、定义异常处理的方法

from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):# Call REST framework's default exception handler first,# to get the standard error response.response = exception_handler(exc, context)# Now add the HTTP status code to the response.if response is not None:response.data['status_code'] = response.status_codereturn response

2、在配置文件中指定自定义的异常处理

REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'wrapper.drf.exception.CustomExceptionHandler',  # 自定义异常处理
}

6.2、REST framework定义的异常

APIException 所有异常的父类
ParseError 解析错误
AuthenticationFailed 认证失败
NotAuthenticated 尚未认证
PermissionDenied 权限决绝
NotFound 未找到
MethodNotAllowed 请求方式不支持
NotAcceptable 要获取的数据格式不支持
Throttled 超过限流次数
ValidationError 校验失败

7、文件上传

图片上传的模型字段
ImageField: 上传图片
FileField: 上传文件(不限文件类型)
模型类代码:

class UploadFile(models.Model):"""文件上传"""file = models.ImageField()def __str__(self):return self.pathclass Meta:db_table = 'upload_file'verbose_name_plural = "文件上传"

序列化器:

# 序列化器
class UploadFileSerializer(serializers.ModelSerializer):"""文件上传"""class Meta:model = UploadFilefields = '__all__'

视图

class UpFileAPIView(ModelViewSet):"""文件上传"""serializer_class = UploadFileSerializerqueryset = UploadFile.objects.all()def create(self, request, *args, **kwargs):"""文件上传"""# 获取文件的大小size = request.data['file'].sizeif size > 1024 * 300:return Response({'msg': "上传失败,文件大小不能超过300kb!", "data":None}, status=400)return super().create(request, *args, **kwargs)# 方式一def retrieve(self, request, pk=None, *args, **kwargs):"""获取文件"""file_obj = self.get_object()response = FileResponse(open(file_obj.file.path, 'rb'))return response# 方式二def upload(self,request, name):file_parh = MEDIA_ROOT / nameresponse = FileResponse(open(file_parh, 'rb'))return response

配置文件 setting.py

# 配置上传的文件保存路径
MEDIA_ROOT = BASE_DIR / 'files'

注意:执行迁移出错时,安装这个库pip install Pillow

六、ajax跨域

针对于前后端分离的项目,前端和后台是分开部署的,因此服务端要支持 CORS(跨域源资源共享) 策略,需要在响应头中加上Access-Control-Allow-Origin: *`
在这里插入图片描述
前端与后端分别是不同的端口,这就涉及到跨域访问数据的问题,因为浏览器的同源策略,默认是不支持两个不同域名间相互访问数据,而我们需要在两个域名间相互传递数据,这时我们就要为后端添加跨域访问的支持。

1、后端:Django配置

1、django-cors-headers

安装

pip install django-cors-headers

添加应用

INSTALLED_APPS = (...'corsheaders',...
)

中间键设置

MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware',...
]

添加白名单

# CORS
# 凡是出现在白名单中的域名,都可以访问后端接口
CORS_ORIGIN_WHITELIST = ('http://127.0.0.1:8848','http://localhost:8080',
)
# 允许所有用户跨域访问
CORS_ORIGIN_ALLOW_ALL = True 或者 CORS_ALLOW_ALL_ORIGINS = True
# CORS_ALLOW_CREDENTIALS 指明在跨域访问中,后端是否支持对cookie的操作。
CORS_ALLOW_CREDENTIALS = True

注意点:
1、浏览器会第一次先发送options请求询问后端是否允许跨域
2、后端在响应结果中告知浏览器允许跨域,允许的情况下浏览器再发送跨域请求

七、DRF JWT

1、token鉴权和JWT介绍

针对前后端分离的项目,ajax跨域请求时,不会自动携带cookie信息,我们不再使用Session认证机
制,而使用JWT(Json Web Token)认证机制,JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。今天给大家介绍JWT的原理和用法

1、token鉴权机制

1、cookie +session鉴权
后端需要去存储用户信息的
2、token鉴权

2、JWT的构成

一个JWT是由三个部分来组成的,头部(header),载荷(payload),签名(signature)。
下面是一个JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo2LCJ1c2VybmFtZSI6Im11c2VuMDA
xIiwiZXhwIjoxNjEwMDg3OTM0LCJlbWFpbCI6Im11c2VuMDAyQHFxLmNvbSJ9.A0rsMrRgiY9_c1lm6_P15Hbx9F95XExmGQhhOzjLytQ
1)、header
在头部中一般包含两部分信息:一部分是类型,一部分是加密算法。
头部数据:

{
'typ': 'JWT',
'alg': 'HS256
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
加密后的头部:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2)、payload
载荷是 JSON Web Token 的主体内容部分,里面存放一些有效信息,JSON Web Token 标准定义了
几个标准字段:

iss: 该JWT的签发者
sub: 该JWT所面向的用户
aud: 接收该JWT的一方
exp: 什么时候过期,这里是一个Unix时间戳
at: 在什么时候签发的

除了标准定义中的字段外,我们还可以自定义字段,比如在 JWT 中,我们的载荷信息可能如下:

{
"sub": "baidu01",
"name": "musen",
"admin": true,
"exp:":12132323423423
}

然后将其进行base64加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3)、signature
签名是 JSON Web Token 中比较重要的一部分,前面两部分都是使用 Base64 进行编码的,signature需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名,签名的作用是保证 JWT 没有被篡改过。
JWT的第三部分签名信息由三部分组成:

header :(base64后的)
payload :(base64后的)
secret: 私钥

加密后的header +加密后的payload 结合私钥secret,用加密算法加密,得到最后的签名

2、simplejwt

1、安装 djangorestframework-simplejwt

pip install djangorestframework-simplejwt

2、 settings.py 中添加配置

# 1、注册到应用中
INSTALLED_APPS = [...'rest_framework_simplejwt',
]
# 2、DRF配置鉴权方式
REST_FRAMEWORK = {
# 配置登录鉴权方式'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',),
}

3、路由中添加登录认证的配置

from rest_framework_simplejwt.views import TokenRefreshView,
TokenVerifyView,TokenObtainPairView
path('login', TokenObtainPairView.as_view(), name='login'), # 登录 (签发)
path('token/refresh', TokenRefreshView.as_view(), name='token_refresh'), #token刷新
path('token/verify', TokenVerifyView.as_view(), name='token_verify'), # token校验

4、token鉴权配置

# settings.py
from datetime import timedelta
SIMPLE_JWT = {"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), # 访问令牌的有效时间"REFRESH_TOKEN_LIFETIME": timedelta(days=1), # 刷新令牌的有效时间"ROTATE_REFRESH_TOKENS": False, # 若为True,则刷新后新的refresh_token有更新的有效时间"BLACKLIST_AFTER_ROTATION": True, # 若为True,刷新后的token将添加到黑名单中,"ALGORITHM": "HS256", # 对称算法:HS256 HS384 HS512 非对称算法:RSA"SIGNING_KEY": SECRET_KEY,"VERIFYING_KEY": None, # if signing_key, verifying_key will be ignore."AUDIENCE": None,"ISSUER": None,'USER_AUTHENTICATION_RULE':'rest_framework_simplejwt.authentication.default_user_authentication_rule',"AUTH_HEADER_TYPES": ("Bearer",), # Authorization: Bearer <token>"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", # if HTTP_X_ACCESS_TOKEN, X_ACCESS_TOKEN: Bearer <token>"USER_ID_FIELD": "id", # 使用唯一不变的数据库字段,将包含在生成的令牌中以标识用户"USER_ID_CLAIM": "user_id",
}

更多配置:
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html

3、登录接口开发

1. 业务说明

验证用户名和密码,验证成功后,为用户签发JWT,前端将签发的JWT保存下来。

2. 后端接口设计

请求方式: POST /login/
请求参数: JSON 或 表单
在这里插入图片描述
返回数据: JSON

{"refresh":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MT
Y2NzM3NzkzMSwiaWF0IjoxNjY3MjkxNTMxLCJqdGkiOiI3NDJiNGI2NzAzNjc0MDFlYWRhZDRiZmRjOW
ExOTU3NiIsInVzZXJfaWQiOjF9.TMRCl7Iiw4F8F6kwByvGDzRIXmPvMljNgEFBvUTLjYI","email": "123@qq.com","username": "admin","id": 1,"token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNj
Y3MjkxODMxLCJpYXQiOjE2NjcyOTE1MzEsImp0aSI6ImRmNjJmZmY5ZDIzNzQ5N2I5ZWRiZTQyMWJlM2
FlYzVkIiwidXNlcl9pZCI6MX0.27HMNYmTnLiipHCs7_7ujIeOIe2KIn0-j7W8Thc1Q5s"
}

在这里插入图片描述

3. 后端实现

路由:

urlpatterns = [url(r'^login/$', MyLoginTokenObtainPairView.as_view()),
]

Django REST framework JWT提供了登录签发JWT的视图TokenObtainPairView,可以直接使用。但是默认的返回值仅有token,我们还需在返回值中增加username和user_id。通过修改该视图的返回值可以完成我们的需求。
在项目中自定义一个模块,创建:

from rest_framework_simplejwt.views import TokenObtainPairView
class MyLoginTokenObtainPairView(TokenObtainPairView):def post(self, request, *args, **kwargs):serializer = self.get_serializer(data=request.data)try:serializer.is_valid(raise_exception=True)except TokenError as e:raise InvalidToken(e.args[0])# 登录成功之后返回的数据信息result = serializer.validated_dataresult['username'] = serializer.user.usernameresult['id'] = serializer.user.idresult['token'] = result.pop('access')return Response(result, status=status.HTTP_200_OK)
http://www.lryc.cn/news/606286.html

相关文章:

  • 《零基础入门AI:传统机器学习核心算法(决策树、随机森林与线性回归)》
  • wxPython 实践(五)高级控件
  • 【ad-hoc构造】P10033 「Cfz Round 3」Sum of Permutation|普及+
  • vscode插件开发(腾讯混元)
  • Go再进阶:结构体、接口与面向对象编程
  • Cesium 快速入门(三)Viewer:三维场景的“外壳”
  • 基于深度学习的医学图像分析:使用BERT实现医学文本分类
  • 零信任网络概念及在网络安全中的应用
  • 【数据库】MySQL 详细安装与基础使用教程(8版本下载及安装)
  • RWA+AI算力賦能全球醫療数字產業升級高峰論壇——暨BitHive BTT 全球發佈會
  • C++面试5题--6day
  • wpf之ContentPresenter
  • PyTorch深度学习快速入门学习总结(三)
  • 【机器学习篇】01day.python机器学习篇Scikit-learn入门
  • 机器学习①【机器学习的定义以及核心思想、数据集:机器学习的“燃料”(组成和获取)】
  • 运行图生视频/文生视频(Wan2.X等)的显卡配置总结
  • 基于deepseek的文本解析 - 超长文本的md结构化
  • CNN卷积神经网络之LeNet和AlexNet经典网络模型(三)
  • 深入解析LLM层归一化:稳定训练的关键
  • 模型优化——在MacOS 上使用 Python 脚本批量大幅度精简 GLB 模型(通过 Blender 处理)
  • 基于PyTorch利用CNN实现MNIST的手写数字识别
  • 【源力觉醒 创作者计划】对比与实践:基于文心大模型 4.5 的 Ollama+CherryStudio 知识库搭建教程
  • 如何系统性了解程序
  • 【Java安全】CC1链
  • <RT1176系列13>LWIP Ping功能入门级应用和基础API解析
  • MySQL 8.0 OCP 1Z0-908 题目解析(41)
  • python制作的软件工具安装包
  • XL2422 无线收发芯片,可用于遥控玩具和智能家居等应用领域
  • 5G-A技术浪潮勾勒通信产业新局,微美全息加快以“5.5G+ AI”新势能深化场景应用
  • 贝锐蒲公英X4 Pro 5G新品路由器:异地组网+8网口+双频WiFi全都有