Skip to content

Commit

Permalink
feat: added batch query display_name by bk_username (#2026)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolin999 authored Dec 28, 2024
1 parent 075dcfa commit 7b2493f
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 4 deletions.
19 changes: 19 additions & 0 deletions src/bk-user/bkuser/apis/open_v3/serializers/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
15 changes: 14 additions & 1 deletion src/bk-user/bkuser/apis/open_v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
]
),
),
]
3 changes: 2 additions & 1 deletion src/bk-user/bkuser/apis/open_v3/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
46 changes: 44 additions & 2 deletions src/bk-user/bkuser/apis/open_v3/views/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
3 changes: 3 additions & 0 deletions src/bk-user/bkuser/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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."
}
}
```
Original file line number Diff line number Diff line change
@@ -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 个对象。"
}
}
```
25 changes: 25 additions & 0 deletions src/bk-user/support-files/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions src/bk-user/tests/apis/open_v3/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)
37 changes: 37 additions & 0 deletions src/bk-user/tests/apis/open_v3/test_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

0 comments on commit 7b2493f

Please sign in to comment.