动态构建django-filter FilterSet

一个动态构建FilterSet的通用方法,可以用在api_view等比较灵活的场景当中

# utils/dynamic_filter.py from typing import Type, Union from django.db.models import QuerySet from django_filters import rest_framework as filters from django.db import models from django.db.models import Model from functools import lru_cache def get_filter_config(model_field, field_name): """ 根据字段类型返回对应的过滤器配置 规则: - 字符类型 → icontains(模糊搜索) - 外键/一对一 → 精确匹配 - 数字类型 → 精确匹配 - 布尔类型 → 精确匹配 - 日期时间 → 支持范围查询 - 多对多 → 包含查询 - UUID → 精确匹配 """ field_type = type(model_field) if field_type in [models.CharField, models.TextField, models.EmailField, models.URLField, models.SlugField]: # 主键、外键和一对一用精确匹配 if model_field.primary_key or isinstance(model_field, (models.ForeignKey, models.OneToOneField)): if isinstance(model_field, (models.ForeignKey, models.OneToOneField)): return filters.ModelChoiceFilter( field_name=field_name, queryset=model_field.related_model.objects.all() ) # 主键使用精确匹配 return filters.CharFilter(field_name=field_name, lookup_expr='exact') return filters.CharFilter(field_name=field_name, lookup_expr='icontains') elif field_type in [models.IntegerField, models.FloatField, models.DecimalField, models.PositiveIntegerField, models.SmallIntegerField, models.BigIntegerField]: return filters.NumberFilter(field_name=field_name) elif field_type == models.BooleanField: return filters.BooleanFilter(field_name=field_name) elif field_type in [models.DateTimeField, models.DateField]: return filters.DateTimeFilter(field_name=field_name) elif field_type == models.ForeignKey: return filters.CharFilter(field_name=field_name, lookup_expr='exact') # 多对多 elif field_type == models.ManyToManyField: return filters.ModelMultipleChoiceFilter( field_name=field_name, queryset=model_field.related_model.objects.all(), ) # UUID elif field_type == models.UUIDField: return filters.UUIDFilter(field_name=field_name) # 默认 else: return filters.CharFilter(field_name=field_name) @lru_cache(maxsize=128) def create_dynamic_filterset(model, filter_fields=(), exclude_fields=(), search_fields=(), search_param='search', ordering_fields=(), ordering_param='order_by'): """ 根据模型和字段列表动态创建 FilterSet(支持过滤 + 排序 + 搜索) Args: model: Django模型类 filter_fields: 需要过滤的字段列表,None表示所有字段 exclude_fields: 排除的字段列表 ordering_fields: 允许排序的字段列表,None表示所有字段 ordering_param: 前端传递排序参数的key,默认 'ordering' search_fields: 允许搜索的字段列表 search_param: 前端传递搜索参数的key,默认 'search' Returns: FilterSet: 动态生成的FilterSet类 """ filter_fields = list(filter_fields) if filter_fields else None exclude_fields = list(exclude_fields) if exclude_fields else None search_fields = list(search_fields) if search_fields else None ordering_fields = list(ordering_fields) if ordering_fields else None if filter_fields is None: filter_fields = [ f.name for f in model._meta.get_fields() if f.concrete and not f.auto_created ] if exclude_fields: filter_fields = [f for f in filter_fields if f not in exclude_fields] # 构建过滤器字典 filters_dict = {} for field_name in filter_fields: try: model_field = model._meta.get_field(field_name) filter_config = get_filter_config(model_field, field_name) if filter_config: filters_dict[field_name] = filter_config except models.FieldDoesNotExist: continue # 确定排序字段 if ordering_fields is None: ordering_fields = [] if ordering_fields: filters_dict[ordering_param] = filters.CharFilter( field_name='ordering', method='filter_ordering', label='排序' ) # 确定搜索字段 手动指定 # if search_fields is None: # # 默认搜索所有字符类型字段(排除外键) # search_fields = [ # f.name for f in model._meta.get_fields() # if f.concrete and not f.auto_created # and isinstance(f, (models.CharField, models.TextField, # models.EmailField, models.URLField, models.SlugField)) # ] # # 如果没有字符字段,添加主键作为搜索字段 # if not search_fields: # search_fields = ['id'] # 添加搜索过滤器 search_fields_list = [] if search_fields: filters_dict[search_param] = filters.CharFilter( field_name='search', method='filter_search', label='搜索' ) search_fields_list = search_fields # 创建 Meta 类 meta_class = type( 'Meta', (), # 父类元组 { 'model': model, 'fields': list(filters_dict.keys()) } ) # 创建 FilterSet 类 class_attrs = filters_dict.copy() class_attrs['Meta'] = meta_class def filter_ordering(self, queryset, name, value): """自定义排序方法 """ if not value: return queryset primary_key = model._meta.pk.name order_fields = [] for field in value.split(','): field = field.strip() if not field: continue if field.startswith('-'): order_field = field[1:] is_desc = True else: order_field = field is_desc = False if order_field in ordering_fields: if is_desc: order_fields.append(f'-{order_field}') else: order_fields.append(order_field) if order_fields: return queryset.order_by(*order_fields) else: return queryset.order_by(f'-{primary_key}') class_attrs['filter_ordering'] = filter_ordering class_attrs['_ordering_fields'] = ordering_fields class_attrs['_ordering_param'] = ordering_param # 添加搜索方法 def filter_search(self, queryset, name, value): """自定义搜索方法""" if not value: return queryset # 构建 OR 查询 from django.db.models import Q q_objects = Q() for field_name in search_fields_list: # 支持跨表搜索(双下划线) if '__' in field_name: q_objects |= Q(**{f'{field_name}__icontains': value}) else: # 检查字段是否存在 try: model._meta.get_field(field_name) q_objects |= Q(**{f'{field_name}__icontains': value}) except models.FieldDoesNotExist: # 忽略不存在的字段 continue return queryset.filter(q_objects) class_attrs['filter_search'] = filter_search class_attrs['_search_fields'] = search_fields_list class_attrs['_search_param'] = search_param # 创建类 filter_class = type( f'{model.__name__}DynamicFilter', (filters.FilterSet,), class_attrs ) return filter_class def build_query(request, model: Type[Model], queryset: QuerySet = None, filter_fields: Union[list, tuple] = (), exclude_fields: Union[list, tuple] = (), search_fields: Union[list, tuple] = (), search_param='search', ordering_fields: Union[list, tuple] = (), ordering_param='order_by'): """ 过滤和排序queryset 传入queryset从queryset中查,否则从model中查 filter_fields: 过滤字段,动态构建过滤器 search_fields: 模糊搜索字段 前端需传参 ?search=... ordering_fields: 排序字段 ordering_param:排序关键字 """ FilterClass = create_dynamic_filterset(model, tuple(filter_fields), tuple(exclude_fields), tuple(search_fields), search_param, tuple(ordering_fields), ordering_param) queryset = model.objects.all() if not queryset else queryset filterset = FilterClass(data=request.query_params, queryset=queryset, request=request) if not filterset.is_valid(): return None return filterset.qs