From 7b2493ffa74d15037e2aeee215107299b68146fc Mon Sep 17 00:00:00 2001 From: rolin999 Date: Sat, 28 Dec 2024 16:38:09 +0800 Subject: [PATCH] feat: added batch query display_name by bk_username (#2026) --- .../bkuser/apis/open_v3/serializers/tenant.py | 19 ++++++ src/bk-user/bkuser/apis/open_v3/urls.py | 15 ++++- .../bkuser/apis/open_v3/views/__init__.py | 3 +- .../bkuser/apis/open_v3/views/tenant.py | 46 +++++++++++++- src/bk-user/bkuser/settings.py | 3 + .../en/batch_query_user_display_name.md | 62 +++++++++++++++++++ .../zh/batch_query_user_display_name.md | 62 +++++++++++++++++++ src/bk-user/support-files/resources.yaml | 25 ++++++++ src/bk-user/tests/apis/open_v3/conftest.py | 8 +++ src/bk-user/tests/apis/open_v3/test_tenant.py | 37 +++++++++++ 10 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md create mode 100644 src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md diff --git a/src/bk-user/bkuser/apis/open_v3/serializers/tenant.py b/src/bk-user/bkuser/apis/open_v3/serializers/tenant.py index 854289cfe..111430012 100644 --- a/src/bk-user/bkuser/apis/open_v3/serializers/tenant.py +++ b/src/bk-user/bkuser/apis/open_v3/serializers/tenant.py @@ -14,9 +14,13 @@ # # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. +from django.conf import settings from rest_framework import serializers from bkuser.apps.tenant.constants import TenantStatus +from bkuser.apps.tenant.models import TenantUser +from bkuser.biz.tenant import TenantUserHandler +from bkuser.common.serializers import StringArrayField class TenantListOutputSLZ(serializers.Serializer): @@ -26,3 +30,18 @@ class TenantListOutputSLZ(serializers.Serializer): class Meta: ref_name = "open_v3.TenantListOutputSLZ" + + +class TenantUserDisplayNameListInputSLZ(serializers.Serializer): + bk_usernames = StringArrayField( + help_text="蓝鲸唯一标识,多个使用逗号分隔", + max_items=settings.BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT, + ) + + +class TenantUserDisplayNameListOutputSLZ(serializers.Serializer): + bk_username = serializers.CharField(help_text="蓝鲸唯一标识", source="id") + display_name = serializers.SerializerMethodField(help_text="用户展示名称") + + def get_display_name(self, obj: TenantUser) -> str: + return TenantUserHandler.generate_tenant_user_display_name(obj) diff --git a/src/bk-user/bkuser/apis/open_v3/urls.py b/src/bk-user/bkuser/apis/open_v3/urls.py index c8701742e..6f8c15ed6 100644 --- a/src/bk-user/bkuser/apis/open_v3/urls.py +++ b/src/bk-user/bkuser/apis/open_v3/urls.py @@ -14,10 +14,23 @@ # # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. -from django.urls import path +from django.urls import include, path from . import views urlpatterns = [ path("tenants/", views.TenantListApi.as_view(), name="open_v3.tenant.list"), + # 租户级别 API + path( + "tenant/", + include( + [ + path( + "users/-/display_name/", + views.TenantUserDisplayNameListApi.as_view(), + name="open_v3.tenant_user.display_name.list", + ) + ] + ), + ), ] diff --git a/src/bk-user/bkuser/apis/open_v3/views/__init__.py b/src/bk-user/bkuser/apis/open_v3/views/__init__.py index f83097fbb..5d8256939 100644 --- a/src/bk-user/bkuser/apis/open_v3/views/__init__.py +++ b/src/bk-user/bkuser/apis/open_v3/views/__init__.py @@ -15,8 +15,9 @@ # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. -from .tenant import TenantListApi +from .tenant import TenantListApi, TenantUserDisplayNameListApi __all__ = [ "TenantListApi", + "TenantUserDisplayNameListApi", ] diff --git a/src/bk-user/bkuser/apis/open_v3/views/tenant.py b/src/bk-user/bkuser/apis/open_v3/views/tenant.py index daa065306..4f0de39b6 100644 --- a/src/bk-user/bkuser/apis/open_v3/views/tenant.py +++ b/src/bk-user/bkuser/apis/open_v3/views/tenant.py @@ -14,12 +14,20 @@ # # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. +import logging + from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status from bkuser.apis.open_v3.mixins import OpenApiCommonMixin -from bkuser.apis.open_v3.serializers.tenant import TenantListOutputSLZ -from bkuser.apps.tenant.models import Tenant +from bkuser.apis.open_v3.serializers.tenant import ( + TenantListOutputSLZ, + TenantUserDisplayNameListInputSLZ, + TenantUserDisplayNameListOutputSLZ, +) +from bkuser.apps.tenant.models import Tenant, TenantUser + +logger = logging.getLogger(__name__) class TenantListApi(OpenApiCommonMixin, generics.ListAPIView): @@ -34,3 +42,37 @@ class TenantListApi(OpenApiCommonMixin, generics.ListAPIView): ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) + + +class TenantUserDisplayNameListApi(OpenApiCommonMixin, generics.ListAPIView): + """ + 批量根据用户 bk_username 获取用户展示名 + TODO: 性能较高,只查询所需字段,后续开发 DisplayName 支持表达式配置时添加 Cache 方案 + """ + + pagination_class = None + + serializer_class = TenantUserDisplayNameListOutputSLZ + + def get_queryset(self): + slz = TenantUserDisplayNameListInputSLZ(data=self.request.query_params) + slz.is_valid(raise_exception=True) + data = slz.validated_data + + # TODO: 由于目前 DisplayName 渲染只与 full_name 相关,所以只查询 full_name + # 后续支持表达式,则需要查询表达式可配置的所有字段 + return ( + TenantUser.objects.filter(id__in=data["bk_usernames"]) + .select_related("data_source_user") + .only("id", "data_source_user__full_name") + ) + + @swagger_auto_schema( + tags=["open_v3.tenant"], + operation_id="batch_query_user_display_name", + operation_description="批量查询用户展示名", + query_serializer=TenantUserDisplayNameListInputSLZ(), + responses={status.HTTP_200_OK: TenantUserDisplayNameListOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 86c3aeeec..052df25b5 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -682,3 +682,6 @@ def _build_file_handler(log_path: Path, filename: str, format: str) -> Dict: ORGANIZATION_SEARCH_API_LIMIT = env.int("ORGANIZATION_SEARCH_API_LIMIT", 20) # 限制批量操作数量上限,避免性能问题 / 误操作(目前不支持跨页全选,最大单页 100 条数据) ORGANIZATION_BATCH_OPERATION_API_LIMIT = env.int("ORGANIZATION_BATCH_OPERATION_API_LIMIT", 100) + +# 限制 bk_username 批量查询 display_name 的数量上限,避免性能问题 +BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT = env.int("BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT", 50) diff --git a/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md b/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md new file mode 100644 index 000000000..ad965cc2c --- /dev/null +++ b/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md @@ -0,0 +1,62 @@ +### Description + +Batch query user's display_name + +### Parameters + +| Name | Type | Required | Description | +|--------------|--------|----------|-----------------------------------------------------------------------------------------------| +| bk_usernames | string | Yes | Blueking unique identifier, multiple identifiers are separated by commas, and the limit is 50 | + +### Request Example + +``` +// URL Query Parameters +bk_usernames=7idwx3b7nzk6xigs,0wngfim3uzhadh1w +``` + +### Response Example for Status Code 200 + +```json5 +{ + "data": [ + { + "bk_username": "7idwx3b7nzk6xigs", + "display_name": "张三", + }, + { + "bk_username": "0wngfim3uzhadh1w", + "display_name": "李四", + } + ] +} +``` + +### Response Parameters Description + +| Name | Type | Description | +|--------------|--------|----------------------------| +| bk_username | string | Blueking unique identifier | +| display_name | string | User's display_name | + +# Response Example for Non-200 Status Code + +```json5 +// status_code = 400 +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "Arguments Validation Failed: bk_usernames: This field cannot be empty." + } +} +``` + +```json5 +// status_code = 400 +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "Arguments Validation Failed: bk_usernames: This field must contain at most 50 objects." + } +} +``` diff --git a/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md b/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md new file mode 100644 index 000000000..a556dba93 --- /dev/null +++ b/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md @@ -0,0 +1,62 @@ +### 描述 + +批量查询用户展示名 + +### 输入参数 + +| 参数名称 | 参数类型 | 必选 | 描述 | +|--------------|--------|----|-------------------------| +| bk_usernames | string | 是 | 蓝鲸唯一标识,多个以逗号分隔,限制数量为 50 | + +### 请求示例 + +``` +// URL Query 参数 +bk_usernames=7idwx3b7nzk6xigs,0wngfim3uzhadh1w +``` + +### 状态码 200 的响应示例 + +```json5 +{ + "data": [ + { + "bk_username": "7idwx3b7nzk6xigs", + "display_name": "张三", + }, + { + "bk_username": "0wngfim3uzhadh1w", + "display_name": "李四", + } + ] +} +``` + +### 响应参数说明 + +| 参数名称 | 参数类型 | 描述 | +|--------------|--------|--------| +| bk_username | string | 蓝鲸唯一标识 | +| display_name | string | 用户展示名 | + +### 状态码非 200 的响应示例 + +```json5 +// status_code = 400 +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "参数校验不通过: bk_usernames: 该字段不能为空。" + } +} +``` + +```json5 +// status_code = 400 +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "参数校验不通过: bk_usernames: 至多包含 50 个对象。" + } +} +``` diff --git a/src/bk-user/support-files/resources.yaml b/src/bk-user/support-files/resources.yaml index 7de450a7d..e4a99574e 100644 --- a/src/bk-user/support-files/resources.yaml +++ b/src/bk-user/support-files/resources.yaml @@ -31,3 +31,28 @@ paths: appVerifiedRequired: true resourcePermissionRequired: false descriptionEn: Query the list of tenants + + /api/v3/open/tenant/users/-/display_name/: + get: + operationId: batch_query_user_display_name + description: 批量查询用户展示名 + tags: [] + responses: + default: + description: '' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: false + matchSubpath: false + backend: + name: default + method: get + path: /api/v3/open/tenant/users/-/display_name/ + matchSubpath: false + timeout: 0 + pluginConfigs: [] + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + resourcePermissionRequired: false + descriptionEn: Batch query user's display_name diff --git a/src/bk-user/tests/apis/open_v3/conftest.py b/src/bk-user/tests/apis/open_v3/conftest.py index 3604ed92f..9fd0f5382 100644 --- a/src/bk-user/tests/apis/open_v3/conftest.py +++ b/src/bk-user/tests/apis/open_v3/conftest.py @@ -20,6 +20,8 @@ from bkuser.apis.open_v3.mixins import OpenApiCommonMixin from rest_framework.test import APIClient +from tests.test_utils.tenant import sync_users_depts_to_tenant + @pytest.fixture def api_client(): @@ -28,3 +30,9 @@ def api_client(): OpenApiCommonMixin, "permission_classes", [] ): yield client + + +@pytest.fixture +def _init_tenant_users_depts(random_tenant, full_local_data_source) -> None: + """初始化租户部门 & 租户用户""" + sync_users_depts_to_tenant(random_tenant, full_local_data_source) diff --git a/src/bk-user/tests/apis/open_v3/test_tenant.py b/src/bk-user/tests/apis/open_v3/test_tenant.py index d51304ded..2a3858f87 100644 --- a/src/bk-user/tests/apis/open_v3/test_tenant.py +++ b/src/bk-user/tests/apis/open_v3/test_tenant.py @@ -15,6 +15,7 @@ # We undertake not to change the open source license (MIT license) applicable # to the current version of the project delivered to anyone in the future. import pytest +from bkuser.apps.tenant.models import TenantUser from django.urls import reverse from rest_framework import status @@ -28,3 +29,39 @@ def test_standard(self, api_client, default_tenant, random_tenant): assert resp.data["count"] == 2 assert {t["id"] for t in resp.data["results"]} == {default_tenant.id, random_tenant.id} assert set(resp.data["results"][0].keys()) == {"id", "name", "status"} + + +@pytest.mark.usefixtures("_init_tenant_users_depts") +class TestTenantUserDisplayNameList: + def test_standard(self, api_client): + zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id + lisi_id = TenantUser.objects.get(data_source_user__code="lisi").id + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, lisi_id])} + ) + + assert resp.status_code == status.HTTP_200_OK + assert len(resp.data) == 2 + assert {t["bk_username"] for t in resp.data} == {zhangsan_id, lisi_id} + assert {t["display_name"] for t in resp.data} == {"张三", "李四"} + + def test_with_invalid_bk_usernames(self, api_client): + zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, "invalid"])} + ) + + assert resp.status_code == status.HTTP_200_OK + assert len(resp.data) == 1 + assert resp.data[0]["bk_username"] == zhangsan_id + assert resp.data[0]["display_name"] == "张三" + + def test_with_no_bk_usernames(self, api_client): + resp = api_client.get(reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ""}) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + def test_with_invalid_length(self, api_client): + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join(map(str, range(1, 52)))} + ) + assert resp.status_code == status.HTTP_400_BAD_REQUEST