1313from django .db .models .fields .files import FieldFile
1414from django .utils .translation import gettext_lazy as _
1515from rest_framework import serializers
16- from rest_framework .fields import ChoiceField
1716from rest_framework .request import Request
18- from rest_framework .serializers import RelatedField , MultipleChoiceField
1917
2018from common .core .filter import get_filter_queryset
2119from common .fields .utils import get_file_absolute_uri
@@ -33,22 +31,56 @@ def func(obj):
3331 return func (obj )
3432
3533
36- class LabeledChoiceField (ChoiceField ):
34+ class LabeledChoiceField (serializers .ChoiceField ):
35+ def __init__ (self , ** kwargs ):
36+ self .attrs = kwargs .pop ("attrs" , None ) or ("value" , "label" )
37+ super ().__init__ (** kwargs )
38+
3739 def to_representation (self , key ):
3840 if key is None :
3941 return key
4042 label = self .choices .get (key , key )
4143 return {"value" : key , "label" : label }
4244
4345 def to_internal_value (self , data ):
46+ if not data :
47+ return data
4448 if isinstance (data , dict ):
4549 data = data .get ("value" )
4650 if isinstance (data , str ) and "(" in data and data .endswith (")" ):
4751 data = data .strip (")" ).split ('(' )[- 1 ]
4852 return super (LabeledChoiceField , self ).to_internal_value (data )
4953
50-
51- class LabeledMultipleChoiceField (MultipleChoiceField ):
54+ def get_schema (self ):
55+ """
56+ 为 drf-spectacular 提供 OpenAPI schema
57+ """
58+ if getattr (self , 'many' , False ):
59+ return {
60+ 'type' : 'array' ,
61+ 'items' : {
62+ 'type' : 'object' ,
63+ 'properties' : {
64+ 'value' : {'type' : 'string' },
65+ 'label' : {'type' : 'string' }
66+ }
67+ },
68+ 'description' : getattr (self , 'help_text' , '' ),
69+ 'title' : getattr (self , 'label' , '' ),
70+ }
71+ else :
72+ return {
73+ 'type' : 'object' ,
74+ 'properties' : {
75+ 'value' : {'type' : 'string' },
76+ 'label' : {'type' : 'string' }
77+ },
78+ 'description' : getattr (self , 'help_text' , '' ),
79+ 'title' : getattr (self , 'label' , '' ),
80+ }
81+
82+
83+ class LabeledMultipleChoiceField (serializers .MultipleChoiceField ):
5284 def __init__ (self , ** kwargs ):
5385 super ().__init__ (** kwargs )
5486 self .choice_mapper = {
@@ -73,7 +105,7 @@ def to_internal_value(self, data):
73105 return data
74106
75107
76- class BasePrimaryKeyRelatedField (RelatedField ):
108+ class BasePrimaryKeyRelatedField (serializers . RelatedField ):
77109 """
78110 Base class for primary key related fields.
79111 """
@@ -89,7 +121,7 @@ def __init__(self, attrs=None, ignore_field_permission=False, **kwargs):
89121 :param attrs: 默认为 None,返回默认的 pk, 一般需要自定义
90122 :param ignore_field_permission: 忽略字段权限控制
91123 """
92- self .attrs = attrs
124+ self .attrs = attrs if attrs else [ "pk" ]
93125 self .label_format = kwargs .pop ("format" , None )
94126 self .input_type = kwargs .pop ("input_type" , None )
95127 self .input_type_prefix = kwargs .pop ("input_type_prefix" , None )
@@ -221,6 +253,122 @@ def to_internal_value(self, data):
221253 except (TypeError , ValueError ):
222254 self .fail ("incorrect_type" , data_type = type (pk ).__name__ )
223255
256+ def get_schema (self ):
257+ """
258+ 为 drf-spectacular 提供 OpenAPI schema
259+ """
260+ # 获取字段的基本信息
261+ field_type = 'array' if self .many else 'object'
262+
263+ if field_type == 'array' :
264+ # 如果是多对多关系
265+ return {
266+ 'type' : 'array' ,
267+ 'items' : self ._get_openapi_item_schema (),
268+ 'description' : getattr (self , 'help_text' , '' ),
269+ 'title' : getattr (self , 'label' , '' ),
270+ }
271+ else :
272+ # 如果是一对一关系
273+ return {
274+ 'type' : 'object' ,
275+ 'properties' : self ._get_openapi_properties_schema (),
276+ 'description' : getattr (self , 'help_text' , '' ),
277+ 'title' : getattr (self , 'label' , '' ),
278+ }
279+
280+ def _get_openapi_item_schema (self ):
281+ """
282+ 获取数组项的 OpenAPI schema
283+ """
284+ return self ._get_openapi_object_schema ()
285+
286+ def _get_openapi_object_schema (self ):
287+ """
288+ 获取对象的 OpenAPI schema
289+ """
290+ properties = {}
291+
292+ # 动态分析 attrs 中的属性类型
293+ for attr in self .attrs :
294+ # 尝试从 queryset 的 model 中获取字段信息
295+ field_type = self ._infer_field_type (attr )
296+ properties [attr ] = {
297+ 'type' : field_type ,
298+ 'description' : f'{ attr } field'
299+ }
300+
301+ return {
302+ 'type' : 'object' ,
303+ 'properties' : properties ,
304+ 'required' : ['id' ] if 'id' in self .attrs else []
305+ }
306+
307+ def _infer_field_type (self , attr_name ):
308+ """
309+ 智能推断字段类型
310+ """
311+ try :
312+ # 如果有 queryset,尝试从 model 中获取字段信息
313+ if hasattr (self , 'queryset' ) and self .queryset is not None :
314+ model = self .queryset .model
315+ if hasattr (model , '_meta' ) and hasattr (model ._meta , 'fields' ):
316+ field = model ._meta .get_field (attr_name )
317+ if field :
318+ return self ._map_django_field_type (field )
319+ except Exception :
320+ pass
321+
322+ # 如果没有 queryset 或无法获取字段信息,使用启发式规则
323+ return self ._heuristic_field_type (attr_name )
324+
325+ def _map_django_field_type (self , field ):
326+ """
327+ 将 Django 字段类型映射到 OpenAPI 类型
328+ """
329+ field_type = type (field ).__name__
330+
331+ # 整数类型
332+ if 'Integer' in field_type or 'BigInteger' in field_type or 'SmallInteger' in field_type :
333+ return 'integer'
334+ # 浮点数类型
335+ elif 'Float' in field_type or 'Decimal' in field_type :
336+ return 'number'
337+ # 布尔类型
338+ elif 'Boolean' in field_type :
339+ return 'boolean'
340+ # 日期时间类型
341+ elif 'DateTime' in field_type or 'Date' in field_type or 'Time' in field_type :
342+ return 'string'
343+ # 文件类型
344+ elif 'File' in field_type or 'Image' in field_type :
345+ return 'string'
346+ # 其他类型默认为字符串
347+ else :
348+ return 'string'
349+
350+ def _heuristic_field_type (self , attr_name ):
351+ """
352+ 启发式推断字段类型
353+ """
354+ # 基于属性名的启发式规则
355+
356+ if attr_name in ['is_active' , 'enabled' , 'visible' ] or attr_name .startswith ('is_' ):
357+ return 'boolean'
358+ elif attr_name in ['count' , 'number' , 'size' , 'amount' ]:
359+ return 'integer'
360+ elif attr_name in ['price' , 'rate' , 'percentage' ]:
361+ return 'number'
362+ else :
363+ # 默认返回字符串类型
364+ return 'string'
365+
366+ def _get_openapi_properties_schema (self ):
367+ """
368+ 获取对象属性的 OpenAPI schema
369+ """
370+ return self ._get_openapi_object_schema ()['properties' ]
371+
224372
225373class PhoneField (serializers .CharField ):
226374
0 commit comments