Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added batch query display_name by bk_username #2026

Merged
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(
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
"tenant/",
include(
[
path(
"users/-/display_name/",
views.TenantUserDisplayNameListApi.as_view(),
name="open_v3.tenant_user.display_name.list",
)
]
),
),
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
]
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
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

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")
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
)
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

@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)},
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
)
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)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading