From ed952d2d3a1fee5c5b8390a8e1b7b210d8be1ad7 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 5 Aug 2026 16:51:59 +0800 Subject: [PATCH] feat(swagger): generate localized static API schema --- apps/edu/views.py | 6 + apps/system/views.py | 1 + apps/third/views.py | 3 + apps/utils/management/__init__.py | 0 apps/utils/management/commands/__init__.py | 0 .../management/commands/build_swagger.py | 71 ++++ apps/utils/swagger.py | 367 ++++++++++++++++++ apps/utils/test_swagger.py | 142 +++++++ apps/utils/viewsets.py | 5 +- server/settings.py | 20 + server/swagger.py | 12 + server/urls.py | 15 +- 12 files changed, 632 insertions(+), 10 deletions(-) create mode 100644 apps/utils/management/__init__.py create mode 100644 apps/utils/management/commands/__init__.py create mode 100644 apps/utils/management/commands/build_swagger.py create mode 100644 apps/utils/swagger.py create mode 100644 apps/utils/test_swagger.py create mode 100644 server/swagger.py diff --git a/apps/edu/views.py b/apps/edu/views.py index c6fa69ef..00da4c56 100644 --- a/apps/edu/views.py +++ b/apps/edu/views.py @@ -71,6 +71,8 @@ class ExamViewSet(CustomModelViewSet): def get_queryset(self): qs = super().get_queryset() + if getattr(self, 'swagger_fake_view', False): + return qs if has_perm(self.request.user, ["exam.view"]): return qs user:User = self.request.user @@ -142,6 +144,8 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, C def get_queryset(self): qs = super().get_queryset() + if getattr(self, 'swagger_fake_view', False): + return qs if has_perm(self.request.user, ["examrecord.view"]): return qs return qs.filter(create_by=self.request.user) @@ -207,6 +211,8 @@ class TrainRecordViewSet(CustomModelViewSet): def get_queryset(self): qs = super().get_queryset() + if getattr(self, 'swagger_fake_view', False): + return qs if has_perm(self.request.user, ["train.view"]): return qs return qs.filter(create_by=self.request.user) diff --git a/apps/system/views.py b/apps/system/views.py index 32742d88..c9783a0d 100755 --- a/apps/system/views.py +++ b/apps/system/views.py @@ -651,6 +651,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet): perms_map = {'get': '*', 'post': 'apk.upload'} serializer_class = ApkSerializer + filter_backends = [] def get_authenticators(self): if self.request.method == 'GET': diff --git a/apps/third/views.py b/apps/third/views.py index 6346e094..3aca1206 100755 --- a/apps/third/views.py +++ b/apps/third/views.py @@ -69,6 +69,7 @@ class SpeakerViewSet(CustomGenericViewSet): """ perms_map = {} serializer_class = serializers.Serializer + filter_backends = [] @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) @@ -125,6 +126,7 @@ class XxTestView(APIView): class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet): perms_map = {'post': '*'} serializer_class = RequestCommonSerializer + filter_backends = [] def create(self, request, *args, **kwargs): """ @@ -258,6 +260,7 @@ class KingCommonViewSet(CreateModelMixin, CustomGenericViewSet): class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): perms_map = {'post': '*'} serializer_class = RequestCommonSerializer + filter_backends = [] def create(self, request, *args, **kwargs): """ diff --git a/apps/utils/management/__init__.py b/apps/utils/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/utils/management/commands/__init__.py b/apps/utils/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/utils/management/commands/build_swagger.py b/apps/utils/management/commands/build_swagger.py new file mode 100644 index 00000000..c47fcf98 --- /dev/null +++ b/apps/utils/management/commands/build_swagger.py @@ -0,0 +1,71 @@ +import json +import os +from io import StringIO +from pathlib import Path + +from django.conf import settings +from django.core.management import BaseCommand, CommandError, call_command + + +class Command(BaseCommand): + help = "生成供 Swagger UI 和 ReDoc 使用的静态 Swagger JSON" + + def add_arguments(self, parser): + parser.add_argument( + "--output", + help="输出路径,默认使用 settings.SWAGGER_SCHEMA_PATH", + ) + parser.add_argument( + "--url", + help="文档中的 API 根地址,默认使用 settings.BASE_URL", + ) + + def handle(self, *args, **options): + target = Path(options["output"] or settings.SWAGGER_SCHEMA_PATH) + if not target.is_absolute(): + target = Path(settings.BASE_DIR) / target + target = target.resolve() + target.parent.mkdir(parents=True, exist_ok=True) + + temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp") + try: + output = StringIO() + call_command( + "generate_swagger", + "-", + format="json", + api_url=options["url"] or settings.BASE_URL, + mock=True, + verbosity=0, + stdout=output, + ) + content = output.getvalue() + schema = json.loads(content) + if schema.get("swagger") != "2.0" or not schema.get("paths"): + raise CommandError("生成的 Swagger 文档缺少版本或接口路径") + + content = json.dumps( + schema, + ensure_ascii=False, + separators=(",", ":"), + ) + temporary.write_text(content, encoding="utf-8") + os.replace(temporary, target) + except Exception as exc: + if isinstance(exc, CommandError): + raise + raise CommandError(f"生成 Swagger 文档失败:{exc}") from exc + finally: + temporary.unlink(missing_ok=True) + + operation_count = sum( + method.lower() in {"get", "post", "put", "patch", "delete"} + for path in schema["paths"].values() + for method in path + ) + self.stdout.write( + self.style.SUCCESS( + f"Swagger文档已生成:{target} " + f"({len(schema['paths'])}个路径,{operation_count}个操作)" + ) + ) diff --git a/apps/utils/swagger.py b/apps/utils/swagger.py new file mode 100644 index 00000000..59448844 --- /dev/null +++ b/apps/utils/swagger.py @@ -0,0 +1,367 @@ +import re +from pathlib import Path + +from django.apps import apps +from django.conf import settings +from django.http import FileResponse, JsonResponse +from drf_yasg import openapi +from drf_yasg.inspectors import FieldInspector, SwaggerAutoSchema +from drf_yasg.inspectors.base import NotHandled + + +CRUD_SUMMARIES = { + "list": "查询{resource}列表", + "retrieve": "查询{resource}详情", + "create": "新增{resource}", + "update": "更新{resource}", + "partial_update": "部分更新{resource}", + "destroy": "删除{resource}", +} + +TAG_NAMES = { + "am": "区域与准入管理", + "asm": "资产管理", + "cm": "标签管理", + "cms": "内容管理", + "develop": "开发工具", + "ecm": "事件管理", + "edu": "培训考试", + "em": "设备管理", + "enm": "能源管理", + "inm": "库存管理", + "mpr": "物资申购与领用", + "mtm": "物料与工艺管理", + "ofm": "办公管理", + "opm": "作业许可", + "pm": "生产任务管理", + "pum": "采购管理", + "qm": "质量管理", + "rem": "研发项目管理", + "rpm": "相关方管理", + "sam": "销售管理", + "third": "第三方集成", + "utils": "通用工具", + "wpm": "生产管理", + "wpmw": "动态产品管理", + "file": "文件管理", +} + +FIELD_NAMES = { + "id": "主键ID", + "ids": "主键ID列表", + "access": "访问令牌", + "refresh": "刷新令牌", + "password_check": "密码确认", + "base64": "Base64数据", + "server_time": "服务器时间", + "timezone": "时区", + "next": "下一页", + "previous": "上一页", + "results": "结果列表", + "detail": "详情", + "items": "明细列表", + "files": "附件列表", + "echart_options": "图表配置", + "tdata_list": "数据列表", + "page": "页码", + "page_size": "每页数量", + "ordering": "排序字段", + "querys": "查询条件列表", + "annotate_field_list": "聚合字段列表", +} + +FIELD_TOKENS = { + "name": "名称", + "code": "编码", + "number": "编号", + "description": "说明", + "note": "备注", + "employee": "人员", + "user": "用户", + "leader": "负责人", + "manager": "负责人", + "keeper": "保管人", + "participant": "参与人", + "post": "岗位", + "dept": "部门", + "belong": "所属", + "create": "创建", + "update": "更新", + "submit": "提交", + "handle": "处理", + "test": "检验", + "material": "物料", + "supplier": "供应商", + "defect": "缺陷", + "equipment": "设备", + "warehouse": "仓库", + "process": "工序", + "operation": "操作", + "state": "状态", + "cate": "分类", + "type": "类型", + "area": "区域", + "team": "班组", + "shift": "班次", + "ticket": "工单", + "file": "文件", + "photo": "照片", + "image": "图片", + "origin": "来源", + "in": "入库", + "out": "出库", + "list": "列表", + "count": "数量", + "total": "总计", + "enabled": "是否启用", +} + +QUERY_PARAMETERS = { + "page": "页码", + "page_size": "每页数量", + "search": "搜索关键字", + "ordering": "排序字段,字段名前加“-”表示倒序", + "format": "响应格式", +} + +LOOKUP_NAMES = { + "in": "属于列表", + "contains": "包含", + "icontains": "包含(忽略大小写)", + "gte": "大于或等于", + "gt": "大于", + "lte": "小于或等于", + "lt": "小于", + "isnull": "是否为空", + "exact": "等于", +} + + +def _contains_chinese(value): + return bool(re.search(r"[\u4e00-\u9fff]", str(value or ""))) + + +def swagger_schema_file(request): + schema_path = Path(settings.SWAGGER_SCHEMA_PATH) + if not schema_path.is_file(): + return JsonResponse( + {"detail": "Swagger文档尚未生成,请先运行 manage.py build_swagger"}, + status=503, + ) + + response = FileResponse( + schema_path.open("rb"), + content_type="application/json; charset=utf-8", + filename="swagger.json", + ) + response["Content-Disposition"] = 'inline; filename="swagger.json"' + response["Cache-Control"] = "no-cache" + return response + + +def _serializer_model(field): + parent = getattr(field, "parent", None) + while parent is not None: + meta = getattr(parent, "Meta", None) + model = getattr(meta, "model", None) + if model is not None: + return model + parent = getattr(parent, "parent", None) + return None + + +def _model_path_label(model, parts): + labels = [] + for part in parts: + if model is None: + break + try: + model_field = model._meta.get_field(part) + except Exception: + break + verbose_name = getattr(model_field, "verbose_name", "") + if _contains_chinese(verbose_name): + labels.append(str(verbose_name)) + model = getattr(model_field, "related_model", None) + return " / ".join(labels) + + +def _field_name_label(field_name): + field_name = str(field_name or "").strip("_") + if field_name in FIELD_NAMES: + return FIELD_NAMES[field_name] + tokens = field_name.split("_") + if tokens and all(token in FIELD_TOKENS for token in tokens): + return "".join(FIELD_TOKENS[token] for token in tokens) + return "" + + +class ChineseFieldInspector(FieldInspector): + """优先使用模型字段中文名称补全 serializer 字段标题。""" + + def field_to_swagger_object(self, field, **kwargs): + return NotHandled + + def process_result(self, result, method_name, obj, **kwargs): + if ( + method_name != "field_to_swagger_object" + or not isinstance(result, openapi.SwaggerDict) + or "$ref" in result + or _contains_chinese(result.get("title")) + ): + return result + + source_attrs = getattr(obj, "source_attrs", None) or [] + model_label = _model_path_label(_serializer_model(obj), source_attrs) + label = model_label or _field_name_label(getattr(obj, "field_name", "")) + field_name = getattr(obj, "field_name", "") + if label: + result["title"] = label + elif field_name: + result["title"] = f"{field_name}(字段)" + return result + + +class ChineseSwaggerAutoSchema(SwaggerAutoSchema): + """为未显式编写文档的接口补充稳定、可读的中文展示信息。""" + + field_inspectors = [ChineseFieldInspector] + SwaggerAutoSchema.field_inspectors + + def get_operation(self, operation_keys=None): + operation = super().get_operation(operation_keys) + model = getattr(getattr(self.view, "queryset", None), "model", None) + for parameter in operation.get("parameters", []): + current = parameter.get("description", "") + if _contains_chinese(current): + continue + location = parameter.get("in") + if location == openapi.IN_BODY: + description = "请求数据" + elif location == openapi.IN_PATH: + description = f"路径参数:{parameter.get('name', '')}" + else: + description = self._get_parameter_description( + parameter.get("name", ""), model + ) + if current: + description = f"{description};{current}" + parameter["description"] = description + return operation + + def get_summary_and_description(self): + summary, description = super().get_summary_and_description() + if summary: + if description and not _contains_chinese(description): + description = f"{summary}\n\n{description}" + return summary, description or summary + + resource = self._get_resource_name() + action = getattr(self.view, "action", None) + template = CRUD_SUMMARIES.get(action) + if template: + summary = template.format(resource=resource) + elif resource: + action_name = str(action or self.method).replace("_", " ") + display_resource = resource + if not _contains_chinese(display_resource): + display_resource = f"{display_resource}接口" + summary = f"{display_resource}:{action_name}" + + if description and not _contains_chinese(description): + description = f"{summary}\n\n{description}" + return summary, description or summary + + def get_request_body_parameters(self, consumes): + parameters = super().get_request_body_parameters(consumes) + for parameter in parameters: + if parameter.get("in") == openapi.IN_BODY and not _contains_chinese( + parameter.get("description") + ): + parameter["description"] = "请求数据" + return parameters + + def get_query_parameters(self): + parameters = super().get_query_parameters() + model = getattr(getattr(self.view, "queryset", None), "model", None) + for parameter in parameters: + current = parameter.get("description", "") + if _contains_chinese(current): + continue + description = self._get_parameter_description( + parameter.get("name", ""), model + ) + if current: + description = f"{description};{current}" + parameter["description"] = description + return parameters + + def get_responses(self): + responses = super().get_responses() + descriptions = { + "200": "请求成功", + "201": "创建成功", + "202": "请求已接受", + "204": "操作成功,无响应内容", + "400": "请求参数错误", + "401": "未认证或认证已失效", + "403": "无权访问", + "404": "资源不存在", + } + for status, response in responses.items(): + if not response.get("description"): + response["description"] = descriptions.get(str(status), "接口响应") + return responses + + def _get_parameter_description(self, name, model): + if name in QUERY_PARAMETERS: + return QUERY_PARAMETERS[name] + + parts = str(name).split("__") + lookup = LOOKUP_NAMES.get(parts[-1]) + field_parts = parts[:-1] if lookup else parts + label = _model_path_label(model, field_parts) + if not label: + label = _field_name_label(field_parts[-1] if field_parts else name) + if not label: + label = f"查询参数:{name}" + if lookup: + label = f"{label}({lookup})" + return label + + def get_tags(self, operation_keys=None): + tags = super().get_tags(operation_keys) + if self.overrides.get("tags") or not tags: + return tags + + if tags[0] in TAG_NAMES: + return [TAG_NAMES[tags[0]]] + + try: + app_config = apps.get_app_config(tags[0]) + except LookupError: + return tags + + if _contains_chinese(app_config.verbose_name): + return [str(app_config.verbose_name)] + return tags + + def _get_resource_name(self): + queryset = getattr(self.view, "queryset", None) + model = getattr(queryset, "model", None) + + if model is None: + serializer_class = getattr(self.view, "serializer_class", None) + meta = getattr(serializer_class, "Meta", None) + model = getattr(meta, "model", None) + + if model is None: + return "接口" + + match = re.search(r"TN\s*[::]\s*([^\n\r]+)", model.__doc__ or "") + if match: + return match.group(1).strip() + + verbose_name = str(model._meta.verbose_name) + if _contains_chinese(verbose_name): + return verbose_name + return model.__name__ diff --git a/apps/utils/test_swagger.py b/apps/utils/test_swagger.py new file mode 100644 index 00000000..27afaeaf --- /dev/null +++ b/apps/utils/test_swagger.py @@ -0,0 +1,142 @@ +import json +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from unittest.mock import patch + +from django.conf import settings +from django.core.management import call_command +from django.test import SimpleTestCase, override_settings + +from apps.am.models import Area +from apps.am.views import AreaViewSet +from apps.utils.swagger import ChineseSwaggerAutoSchema, swagger_schema_file + + +class ChineseSwaggerAutoSchemaTests(SimpleTestCase): + def make_schema(self, view, method="GET"): + schema = ChineseSwaggerAutoSchema.__new__(ChineseSwaggerAutoSchema) + schema.view = view + schema.method = method + schema.path = "/am/area/" + schema.overrides = {} + schema.operation_keys = ("am", "area", "list") + schema._sch = SimpleNamespace(get_description=lambda path, method: "") + return schema + + def test_crud_summary_uses_model_chinese_name(self): + view = SimpleNamespace(queryset=Area.objects.all(), action="list") + schema = self.make_schema(view) + + summary, description = schema.get_summary_and_description() + + self.assertEqual(summary, "查询地图区域列表") + self.assertEqual(description, "查询地图区域列表") + + def test_explicit_summary_takes_priority(self): + view = SimpleNamespace(queryset=Area.objects.all(), action="list") + schema = self.make_schema(view) + schema.overrides = { + "operation_summary": "区域自定义查询", + "operation_description": "自定义说明", + } + + summary, description = schema.get_summary_and_description() + + self.assertEqual(summary, "区域自定义查询") + self.assertEqual(description, "自定义说明") + + def test_custom_action_with_english_model_name_has_chinese_hint(self): + model = SimpleNamespace( + __doc__="", + __name__="Dataset", + _meta=SimpleNamespace(verbose_name="dataset"), + ) + queryset = SimpleNamespace(model=model) + view = SimpleNamespace(queryset=queryset, action="base") + schema = self.make_schema(view) + + summary, _ = schema.get_summary_and_description() + + self.assertEqual(summary, "Dataset接口:base") + + def test_tag_uses_chinese_business_module_name(self): + view = SimpleNamespace(queryset=Area.objects.all(), action="list") + schema = self.make_schema(view) + + self.assertEqual(schema.get_tags(("am", "area", "list")), ["区域与准入管理"]) + + def test_filter_parameter_uses_model_field_labels(self): + view = SimpleNamespace(queryset=Area.objects.all(), action="list") + schema = self.make_schema(view) + + description = schema._get_parameter_description( + "manager__name__contains", + Area, + ) + + self.assertIn("区域负责人", description) + self.assertIn("包含", description) + + def test_swagger_queryset_skips_permission_data_lookup(self): + view = AreaViewSet(basename="area") + view.action = "list" + view.swagger_fake_view = True + + with patch("apps.utils.viewsets.get_user_perms_map") as permission_lookup: + queryset = view.get_queryset() + + self.assertIs(queryset.model, Area) + permission_lookup.assert_not_called() + + +class SwaggerSettingsTests(SimpleTestCase): + def test_swagger_supports_jwt_authorization_header(self): + from django.conf import settings + + bearer = settings.SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["Bearer"] + + self.assertEqual(bearer["type"], "apiKey") + self.assertEqual(bearer["name"], "Authorization") + self.assertEqual(bearer["in"], "header") + + def test_swagger_ui_uses_static_schema(self): + from django.conf import settings + + self.assertEqual(settings.SWAGGER_SETTINGS["SPEC_URL"], "schema-swagger-json") + self.assertEqual(settings.REDOC_SETTINGS["SPEC_URL"], "schema-swagger-json") + + +class BuildSwaggerCommandTests(SimpleTestCase): + def test_command_writes_valid_utf8_schema(self): + schema = { + "swagger": "2.0", + "info": {"title": "中文文档"}, + "paths": {"/demo/": {"get": {}}}, + } + + def generate_schema(command_name, output_file, **options): + self.assertEqual(command_name, "generate_swagger") + self.assertEqual(output_file, "-") + options["stdout"].write(json.dumps(schema, ensure_ascii=False)) + + with TemporaryDirectory(dir=settings.BASE_DIR) as directory: + target = Path(directory) / "swagger.json" + with override_settings(SWAGGER_SCHEMA_PATH=str(target)): + with patch( + "apps.utils.management.commands.build_swagger.call_command", + side_effect=generate_schema, + ): + call_command("build_swagger", verbosity=0) + + content = target.read_text(encoding="utf-8") + self.assertIn("中文文档", content) + self.assertEqual(json.loads(content), schema) + + with override_settings(SWAGGER_SCHEMA_PATH=str(target)): + response = swagger_schema_file(SimpleNamespace()) + body = b"".join(response.streaming_content) + response.close() + + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(body), schema) diff --git a/apps/utils/viewsets.py b/apps/utils/viewsets.py index a4246422..93623816 100755 --- a/apps/utils/viewsets.py +++ b/apps/utils/viewsets.py @@ -154,6 +154,9 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): def get_queryset(self): queryset = super().get_queryset() queryset = self.get_queryset_custom(queryset) + # drf-yasg 生成文档时不应读取权限或业务数据。 + if getattr(self, 'swagger_fake_view', False): + return queryset if self.data_filter: user = self.request.user if user.is_superuser: @@ -232,4 +235,4 @@ class EuModelViewSet(BulkCreateModelMixin, CustomListModelMixin, CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet): """ 不支持更新的增强ModelViewSet - """ \ No newline at end of file + """ diff --git a/server/settings.py b/server/settings.py index d0bf321c..1df8238b 100755 --- a/server/settings.py +++ b/server/settings.py @@ -178,6 +178,7 @@ USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static') +SWAGGER_SCHEMA_PATH = os.path.join(STATIC_ROOT, 'openapi/swagger.json') # STATICFILES_DIRS = ( # os.path.join(BASE_DIR, 'dist/static'), # ) @@ -267,8 +268,27 @@ CELERYD_SOFT_TIME_LIMIT = 60*10 # swagger配置 SWAGGER_SETTINGS = { + 'DEFAULT_INFO': 'server.swagger.api_info', + 'DEFAULT_API_URL': BASE_URL, + 'SPEC_URL': 'schema-swagger-json', 'LOGIN_URL': '/django/admin/login/', 'LOGOUT_URL': '/django/admin/logout/', + 'DEFAULT_AUTO_SCHEMA_CLASS': 'apps.utils.swagger.ChineseSwaggerAutoSchema', + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header', + 'description': 'JWT认证,请输入:Bearer ', + }, + 'Basic': { + 'type': 'basic', + }, + }, +} + +REDOC_SETTINGS = { + 'SPEC_URL': 'schema-swagger-json', } # 日志配置 diff --git a/server/swagger.py b/server/swagger.py new file mode 100644 index 00000000..c1c20132 --- /dev/null +++ b/server/swagger.py @@ -0,0 +1,12 @@ +from django.conf import settings +from drf_yasg import openapi + +from server.settings import get_sysconfig + + +api_info = openapi.Info( + title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}', + default_version=settings.SYS_VERSION, + contact=openapi.Contact(email="caoqianming@foxmail.com"), + license=openapi.License(name="MIT License"), +) diff --git a/server/urls.py b/server/urls.py index ac46779f..b4fab73e 100755 --- a/server/urls.py +++ b/server/urls.py @@ -17,19 +17,14 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework.documentation import include_docs_urls from django.views.generic import TemplateView -from server.settings import get_sysconfig +from apps.utils.swagger import swagger_schema_file +from server.swagger import api_info schema_view = get_schema_view( - openapi.Info( - title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}', - default_version=settings.SYS_VERSION, - contact=openapi.Contact(email="caoqianming@foxmail.com"), - license=openapi.License(name="MIT License"), - ), + api_info, public=True, permission_classes=[], url=settings.BASE_URL @@ -89,8 +84,10 @@ urlpatterns = [ if getattr(settings, 'ENABLE_SWAGGER', True): urlpatterns += [ # api文档 + path('api/swagger.json', swagger_schema_file, + name='schema-swagger-json'), path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - ] \ No newline at end of file + ]