DRF 商城项目 - 用户( 登陆, 注册,登出,我的中心 ) 逻辑梳理

用户登陆

自定义用户登陆字段处理

用户的登陆时经过 手机号也能够进行登陆html

须要重写登陆验证逻辑前端

from django.contrib.auth.backends import ModelBackend

class CustomBackend(ModelBackend):

    def authenticate(self, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(Q(username=username) | Q(mobile=username))
            # 前端的用户传递过来的密码和数据库的保存密码是不一致的, 所以须要使用 check_password 的方式进行比对
            if user.check_password(password):
                return user
        except Exception as e:
            return None

登陆逻辑

经过 login 接口进入验证, 调用默认重写后的验证逻辑进行处理python

 url(r'^login/', obtain_jwt_token)

验证成功后会返回 token数据库

用户注册

用户注册基于 手机号注册django

验证码发送基于 云片网 提供的技术支持json

验证码逻辑

验证码API 接口

# 配置手机验证码发送 的 url
router.register(r'codes', SmsCodeViewset, base_name="codes")

验证码序列化组件

选取序列化方式的时候觉得不是所有的字段都须要用上, 所以不需用到 ModelSerializer后端

须要对前端拿到的  mobile 字段进行相关的验证api

是否注册, 是否合法, 以及频率限制服务器

# 手机验证序列化组件
# 不使用 ModelSerializer, 并不须要全部的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    # 验证手机号码
    # validate_ + 字段名 的格式命名
    def validate_mobile(self, mobile):

        # 手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号码是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码非法")

        # 验证码发送频率
        # 当前时间减去一分钟( 倒退一分钟 ), 而后发送时间要大于这个时间, 表示还在一分钟内
        one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")
        return mobile

验证码视图

视图主要处理 验证码生成发送相关逻辑cookie

具体的云片网接口对接处理详情官网查阅

# 发送短信验证码
class SmsCodeViewset(CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = SmsSerializer

    # 生成四位数字的验证码
    def generate_code(self):

        seeds = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))
        return "".join(random_str)

    # 重写 create 方法
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # 验证后便可取出数据
        mobile = serializer.validated_data["mobile"]
        yun_pian = YunPian(APIKEY)
        code = self.generate_code()
        sms_status = yun_pian.send_sms(code=code, mobile=mobile)

        if sms_status["code"] != 0:
            return Response({
                "mobile": sms_status["msg"]
            }, status=status.HTTP_400_BAD_REQUEST)
        else:
            # 确认无误后须要保存数据库中
            code_record = VerifyCode(code=code, mobile=mobile)
            code_record.save()
            return Response({
                "mobile": mobile
            }, status=status.HTTP_201_CREATED)

云片验证码工具文件

# _*_ coding:utf-8 _*_
from YtShop.settings import APIKEY

__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"
import requests
import json


# 云片网短信发送功能类
class YunPian(object):

    def __init__(self, api_key):
        self.api_key = api_key
        self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"

    def send_sms(self, code, mobile):
        parmas = {
            "apikey": self.api_key,
            "mobile": mobile,
            "text": "您的验证码是{code}。如非本人操做,请忽略本短信".format(code=code)
        }

        response = requests.post(self.single_send_url, data=parmas)
        re_dict = json.loads(response.text)
        return re_dict


if __name__ == "__main__":
    yun_pian = YunPian(APIKEY)
    yun_pian.send_sms("2019", "")  # 参数为 code 以及 mobile

配置文件

须要用到两个配置添加

# 手机号码的验证正则式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

# 云片网的 APIKEY 设置
APIKEY = "2480f562xxxxxxxxxxxxxcb7673f8"

注册逻辑

注册 API 接口

# 配置用户注册的 url
router.register(r'users', UserViewset, base_name="users")

注册序列化组件

用户注册须要的字段较多

每一个字段都有些独有的特殊裁定

用户名  要进行重复判断

验证码  要进行有效期, 正确性判断

密码  设置 输入框为密码格式

在最后回传的时候 code 是不须要的, 所以能够删除掉

# 用户注册
class UserRegSerializer(serializers.ModelSerializer):
    """
    max_length      最大长度
    min_length      最小长度
    label           显示名字
    help_text       帮助提示信息
    error_messages  错误类型映射提示
        blank         空字段提示
        required      必填字段提示
        max_length    超长度提示
        min_length    太短提示
    write_only      只读, 序列化的时候忽略字段, 再也不返回给前端页面, 用于去除关键信息(密码等)或者某些没必要要字段(验证码)
    style           更改输入标签显示类型
    validators      能够指明一些默认的约束类
        UniqueValidator             约束惟一
        UniqueTogetherValidator     联合约束惟一
        UniqueForMonthValidator
        UniqueForDateValidator
        UniqueForYearValidator
        ....
    """
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 help_text="验证码")

    # validators 能够指明一些默认的约束类, 此处的 UniqueValidator 表示惟一约束限制不能重名
    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

    # style 能够设置为密文状态
    password = serializers.CharField(
        style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
    )

    # 用户表中的 password 是须要加密后再保存的, 次数须要重写一次 create 方法
    # 固然也能够不这样作, 这里的操做利用 django 的信号来处理, 详情见 signals.py
    # def create(self, validated_data):
    #     user = super(UserRegSerializer, self).create(validated_data=validated_data)
    #     user.set_password(validated_data["password"])
    #     user.save()
    #     return user

    # 对验证码的验证处理
    # validate_ + 字段对个别字段进行单一处理
    def validate_code(self, code):

        # 若是使用 get 方式须要处理两个异常, 分别是查找到多个信息的状况以及查询到0信息的状况的异常
        # 可是使用 filter 方式查到多个就以列表方式返回, 若是查询不到数据就会返回空值, 各方面都很方便
        # try:
        #     verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
        # except VerifyCode.DoesNotExist as e:
        #     pass
        # except VerifyCode.MultipleObjectsReturned as e:
        #     pass

        # 前端传过来的全部的数据都在, initial_data 字典里面, 若是是验证经过的数据则保存在 validated_data 字典中
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_record = verify_records[0]  # 时间倒叙排序后的的第一条就是最新的一条
            # 当前时间回退5分钟
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            # 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过时
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过时")
            # 根据记录的 验证码 比对判断
            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")
            # return code  # 不必保存验证码记录, 仅仅是用做验证
        else:
            raise serializers.ValidationError("验证码错误")

    # 对全部的字段进行限制
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]  # 重命名一下
        del attrs["code"]  # 删除无用字段
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile", "password")

注册视图

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    # 重写 create 函数来完成注册后自动登陆功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
        
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        # token 的添加只能用此方法, 此方法经过源码阅读查找到位置为
        re_dict["token"] = jwt_encode_handler(payload)
        # 自定义一个字段加入进去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    def get_object(self):
        return self.request.user

    def perform_create(self, serializer):
        return serializer.save()

信号量处理工具文件

注册后的信息回传给数据库保存的时候 密码是按照是未加密状态保存

此处须要进行加密后才能够, 所以这里能够用信号量来处理, post_save 触发

在此触发流程中完成加密后保存数据库

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"

from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model

User = get_user_model()


@receiver(post_save, sender=User)  # post_save 信号类型, sender 能触发信号的模型
def create_user(sender, instance=None, created=False, **kwargs):    # created 是否新建( update 就不会被识别 )
    # instance 表示保存对象, 在这里是被保存的 user 对象
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()
        # Token.objects.create(user=instance)
        # user 对象的保存通常是要伴随着 token 的, 这里已经使用 JWT 方式了, 所以就不须要这种 token 了.

注册后自动登陆逻辑

目标预期

用户注册后自动跳转到主页

同时要实现注册用户已登陆状态

需求分析

用户注册相关的操做本质是从前端拿到数据传送到后端经过 相关的 view 进行操做

本质是 底层的 create 方法, 默认的方法只能实现用户建立没法实现其余附加

( DRF 的视图 功能嵌套 层次详情点击 这里查看  )

所以咱们须要重写 create 方法 

定位重写 create 方法

可见只有序列化类的更新和推送, 无其余功能

默认的 create 方法

若是想实现自动登陆, 首先本质就是加入用户登陆的状态, 即 token 的生成和保存

本次项目使用的是 JWT 做为 token 方案, 所以 须要考究在 JWT 的源码中 token 如何生成

定位 token 生成源码查阅

JWT 的源码入口 ( URL 对接视图 )

往上找到视图类

这里是作了一层很简单的封装, 以及能够看到熟悉的 as_view()

不过咱们目前不关心这个, 这里一样基于 DRF 视图中相似

视图类中找到序列化处理

 这个 serializer_class 就是对应着序列化类的处理

 

序列化处理中对 token 的处理

其实咱们已经知道了JWT 的方式是不会基于数据库的, 所以他们的序列化类中的是没有任何的字段

经过各类方法来实现字段的计算和生成

如下是所有的 相关逻辑

class JSONWebTokenSerializer(Serializer):
    """
    Serializer class used to validate a username and password.

    'username' is identified by the custom UserModel.USERNAME_FIELD.

    Returns a JSON Web Token that can be used to authenticate later calls.
    """
    def __init__(self, *args, **kwargs):
        """
        Dynamically add the USERNAME_FIELD to self.fields.
        """
        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)

        self.fields[self.username_field] = serializers.CharField()
        self.fields['password'] = PasswordField(write_only=True)

    @property
    def username_field(self):
        return get_username_field()

    def validate(self, attrs):
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            user = authenticate(**credentials)

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)

                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

定位到 token 的生成代码

可见 须要使用到 jwt_payload_handler 方法以及 jwt_encode_handler 方法

所以生成 token 就是在这里了, 为了生成 token 咱们须要用到这两个方法, 使用方法就彻底模仿源码便可

完成 create 重写

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    # 重写 create 函数来完成注册后自动登陆功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
   
       # 此处为自定义的 token 的生成 
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        re_dict["token"] = jwt_encode_handler(payload)
        # 顺便把 用户名一并传过去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    def get_object(self):
        return self.request.user

    def perform_create(self, serializer):
        return serializer.save()

用户退出

不须要再写一个 logout 接口 

JWT 不须要服务器这边进行相关的操做

只须要前端进行一个 cookie 的清空而后跳转便可

跳转到 登陆页面或者主页皆可

    loginOut(){
        cookie.delCookie('token');
        cookie.delCookie('name');
        //从新触发store
        //更新store数据
        this.$store.dispatch('setInfo');
        //跳转到登陆
        this.$router.push({name: 'login'})
      },

用户我的中心 

retrieve 方式添加

用户中心的数据来源是对单一用户的详细数据请求, 所以须要在原有基础上加上对  retrieve 的处理

 mixins.RetrieveModelMixin

用户 id 传递

同时由于对单一用户的请求须要指明用户id, 有两种方式能够传递

第一种 直接在数据里面提供当前用户 id

第二种 重写 get_object 获取当前用户

# 由于要涉及到 我的中心的操做须要传递过去 用户的 id, 重写 get_object 来实现
    def get_object(self):
        return self.request.user

权限分离

用户中心必须指定当前用户只能访问本身, 所以须要对是否登陆进行验证

可是当前视图的其余类型请求好比 create 的注册则不须要进行验证, 所以  permission_classes 没法知足需求

源码剖析

在继承了  ViewSetMixin 以后内部的 initialize_request 方面里面的 提供了 .action 在 request 中能够对请求类型进行分离

同时 APIView 内部的  get_permissions  方法负责提取认证类型, 所以重写此方法便可完成

 

此为 源码, 可见是直接使用一个列表表达式来获取当前视图的 permission_classes 里面的全部认证方式

实现重写

基于咱们本身的需求进行重写, 利用 action 进行分流

注意其余未设置的最后必定要返回空

    # permission_classes = (permissions.IsAuthenticated, )  # 由于根据类型的不一样权限的认证也不一样, 不能再统一设置了
    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []

序列化组件分离

建立组件

以前设置的序列化组件是为了注册用的, 只采集了注册相关的字段, 没法知足用户中心的其余字段处理

所以须要从新设置一个用户详情的 序列化组件

# 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("name", "gender", "birthday", "email", "mobile")

源码剖析

一样是基于对 action 的方法进行分流, 对于 action 的位置在 权限分流的部分有图,

在   GenericAPIView 中存在 get_serializer_class  方法, 用于获取当前视图中的 序列化组件

实现重写

基于 action 进行分流, 而后进行对 get_serializer_class 进行重写

实现方式相似于 权限的分流

   def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer
        return UserDetailSerializer

完整代码

用户视图代码

# 用户视图
class UserViewset(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication)

    # 用户中心的我的详情数据不能再基于统一设置的 UserRegSerializer 了
    # 用户注册和 用户详情分为了两个序列化组件
    # self.action 必需要继承了 ViewSetMixin 才有此功能
    # get_serializer_class 的源码位置在 GenericAPIView 中
    def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer
        return UserDetailSerializer

    # permission_classes = (permissions.IsAuthenticated, )  # 由于根据类型的不一样权限的认证也不一样, 不能再统一设置了
    # get_permissions 的源码在 APIview 中
    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []

    # 重写 create 函数来完成注册后自动登陆功能
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)

        """
        此处重写的源码分析以及 相关的逻辑
        详情点击此博客 
        https://www.cnblogs.com/shijieli/p/10726194.html
        """
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        # token 的添加只能用此方法, 此方法经过源码阅读查找到位置为
        re_dict["token"] = jwt_encode_handler(payload)
        # 自定义一个字段加入进去
        re_dict["name"] = user.name if user.name else user.username

        headers = self.get_success_headers(serializer.data)
        return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

    # 由于要涉及到 我的中心的操做须要传递过去 用户的 id, 重写 get_object 来实现
    def get_object(self):
        return self.request.user

    def perform_create(self, serializer):
        return serializer.save()

用户相关序列化组件

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25"

import re
from rest_framework import serializers
from django.contrib.auth import get_user_model
from datetime import datetime
from datetime import timedelta
from rest_framework.validators import UniqueValidator

from .models import VerifyCode
from YtShop.settings import REGEX_MOBILE

User = get_user_model()


# 手机验证序列化组件
# 不使用 ModelSerializer, 并不须要全部的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    # 验证手机号码
    # validate_ + 字段名 的格式命名
    def validate_mobile(self, mobile):

        # 手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号码是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码非法")

        # 验证码发送频率
        # 当前时间减去一分钟( 倒退一分钟 ), 而后发送时间要大于这个时间, 表示还在一分钟内
        one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")
        return mobile


# 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("name", "gender", "birthday", "email", "mobile")


# 用户注册
class UserRegSerializer(serializers.ModelSerializer):
    """
    max_length      最大长度
    min_length      最小长度
    label           显示名字
    help_text       帮助提示信息
    error_messages  错误类型映射提示
        blank         空字段提示
        required      必填字段提示
        max_length    超长度提示
        min_length    太短提示
    write_only      只读, 序列化的时候忽略字段, 再也不返回给前端页面, 用于去除关键信息(密码等)或者某些没必要要字段(验证码)
    style           更改输入标签显示类型
    validators      能够指明一些默认的约束类
        UniqueValidator             约束惟一
        UniqueTogetherValidator     联合约束惟一
        UniqueForMonthValidator
        UniqueForDateValidator
        UniqueForYearValidator
        ....
    """
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 help_text="验证码")

    # validators 能够指明一些默认的约束类, 此处的 UniqueValidator 表示惟一约束限制不能重名
    username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

    # style 能够设置为密文状态
    password = serializers.CharField(
        style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
    )

    # 用户表中的 password 是须要加密后再保存的, 次数须要重写一次 create 方法
    # 固然也能够不这样作, 这里的操做利用 django 的信号来处理, 详情见 signals.py
    # def create(self, validated_data):
    #     user = super(UserRegSerializer, self).create(validated_data=validated_data)
    #     user.set_password(validated_data["password"])
    #     user.save()
    #     return user

    # 对验证码的验证处理
    # validate_ + 字段对个别字段进行单一处理
    def validate_code(self, code):

        # 若是使用 get 方式须要处理两个异常, 分别是查找到多个信息的状况以及查询到0信息的状况的异常
        # 可是使用 filter 方式查到多个就以列表方式返回, 若是查询不到数据就会返回空值, 各方面都很方便
        # try:
        #     verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
        # except VerifyCode.DoesNotExist as e:
        #     pass
        # except VerifyCode.MultipleObjectsReturned as e:
        #     pass

        # 前端传过来的全部的数据都在, initial_data 字典里面 ,
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_record = verify_records[0]  # 时间倒叙排序后的的第一条就是最新的一条
            # 当前时间回退5分钟
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            # 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过时
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过时")
            # 根据记录的 验证码 比对判断
            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")
            # return code  # 不必保存验证码记录, 仅仅是用做验证
        else:
            raise serializers.ValidationError("验证码错误")

    # 对全部的字段进行限制
    def validate(self, attrs):
        attrs["mobile"] = attrs["username"]  # 重命名一下
        del attrs["code"]  # 删除无用字段
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile", "password")
相关文章
相关标签/搜索