Skip to content

Commit c99bec0

Browse files
authored
refactor(open api v2 - list user): reduce unnecessary fields in SQL S… (#2312)
1 parent fea6822 commit c99bec0

4 files changed

Lines changed: 205 additions & 81 deletions

File tree

src/bk-user/bkuser/apis/open_v2/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
views.ProfileDepartmentListApi.as_view(),
3939
name="open_v2.list_profile_departments",
4040
),
41-
# 与上面 API 一样,只是兼容了缺少末尾 / 的情况 (ESB yaml 配置里是该情况)
41+
# 与上面 API 一样,只是兼容了缺少末尾 / 的情况(ESB yaml 配置里是该情况)
4242
path(
4343
"profiles/<str:lookup_value>/departments",
4444
views.ProfileDepartmentListApi.as_view(),
@@ -66,7 +66,7 @@
6666
views.DepartmentProfileListApi.as_view(),
6767
name="open_v2.list_department_profiles",
6868
),
69-
# 与上面 API 一样,只是兼容了缺少末尾 / 的情况 (ESB yaml 配置里是该情况)
69+
# 与上面 API 一样,只是兼容了缺少末尾 / 的情况(ESB yaml 配置里是该情况)
7070
path(
7171
"departments/<str:id>/profiles",
7272
views.DepartmentProfileListApi.as_view(),

src/bk-user/bkuser/apis/open_v2/views/profilers.py

Lines changed: 190 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -86,80 +86,202 @@ def build_user_infos(self, tenant_users: QuerySet[TenantUser], fields: List[str]
8686
且必须保证 select_related("data_source_user")
8787
:param fields: 对外的用户字段列表,空时表示所有用户字段都对外
8888
"""
89-
# 按需提前获取用户 Leader 信息 和 用户部门信息
89+
# 标准化字段列表
90+
fields = self._get_default_fields(fields)
91+
92+
# 优化查询:只查询需要的数据库字段
93+
tenant_users = self._optimize_queryset(tenant_users, fields)
94+
95+
# 批量预加载关联数据
96+
context = self._prepare_context(tenant_users, fields)
97+
98+
# 构建每个用户的信息
99+
return [self._build_single_user_info(tenant_user, fields, context) for tenant_user in tenant_users]
100+
101+
@staticmethod
102+
def _get_default_fields(fields: List[str]) -> List[str]:
103+
"""获取默认字段列表"""
104+
return fields or [
105+
"id",
106+
"username",
107+
"display_name",
108+
"email",
109+
"telephone",
110+
"country_code",
111+
"time_zone",
112+
"language",
113+
"wx_userid",
114+
"domain",
115+
"category_id",
116+
"status",
117+
"extras",
118+
"position",
119+
"leader",
120+
"departments",
121+
]
122+
123+
@staticmethod
124+
def _get_db_field_map() -> Dict[str, List[str]]:
125+
"""获取字段到数据库字段的映射"""
126+
return {
127+
"id": ["data_source_user__id"],
128+
"username": ["id"],
129+
"display_name": ["data_source_user__full_name"],
130+
"email": ["custom_email", "is_inherited_email", "data_source_user__email"],
131+
"telephone": ["custom_phone", "is_inherited_phone", "data_source_user__phone"],
132+
"country_code": [
133+
"custom_phone_country_code",
134+
"is_inherited_phone",
135+
"data_source_user__phone_country_code",
136+
],
137+
"time_zone": ["time_zone"],
138+
"language": ["language"],
139+
"wx_userid": ["wx_userid"],
140+
"domain": ["data_source_id", "tenant_id"],
141+
"category_id": ["data_source_id"],
142+
"status": ["status"],
143+
"extras": ["data_source_user__extras"],
144+
"position": ["data_source_user__extras"],
145+
"leader": ["data_source_user__id"],
146+
"departments": ["data_source_user__id"],
147+
}
148+
149+
def _optimize_queryset(self, tenant_users: QuerySet[TenantUser], fields: List[str]) -> QuerySet[TenantUser]:
150+
"""优化查询,只查询需要的数据库字段"""
151+
db_field_map = self._get_db_field_map()
152+
only_fields = {"id"}
153+
for f in fields:
154+
only_fields.update(db_field_map.get(f, []))
155+
return tenant_users.only(*only_fields)
156+
157+
def _prepare_context(self, tenant_users: QuerySet[TenantUser], fields: List[str]) -> Dict[str, Any]:
158+
"""预加载关联数据,减少数据库查询"""
90159
data_source_user_ids = [i.data_source_user.id for i in tenant_users]
91-
leader_map = self._get_leader_map(data_source_user_ids) if not fields or "leader" in fields else {}
92-
department_map = (
93-
self._get_department_map(data_source_user_ids) if not fields or "departments" in fields else {}
94-
)
95160

96-
collaboration_field_mapping = self.get_collaboration_field_mapping()
97-
98-
user_infos = []
99-
for tenant_user in tenant_users:
100-
# 手机号和手机区号
101-
phone, phone_country_code = tenant_user.phone_info
102-
103-
# 自定义字段
104-
source_extras = tenant_user.data_source_user.extras
105-
# 协同时按照协同租户配置的用户自定义字段进行输出
106-
ds_owner_tenant_id = tenant_user.data_source.owner_tenant_id
107-
if ds_owner_tenant_id != tenant_user.tenant_id:
108-
extras = {
109-
collaboration_field_mapping[(ds_owner_tenant_id, k)]: v
110-
for k, v in source_extras.items()
111-
if (ds_owner_tenant_id, k) in collaboration_field_mapping
112-
}
113-
else:
114-
extras = source_extras
115-
116-
# 不会放大查询的字段
117-
user_info = {
118-
"id": tenant_user.data_source_user.id,
119-
# 租户用户 ID 即为对外的 username / bk_username
120-
"username": tenant_user.id,
121-
"display_name": tenant_user.data_source_user.full_name,
122-
"email": tenant_user.email,
123-
"telephone": phone,
124-
"country_code": phone_country_code,
125-
"iso_code": _phone_country_code_to_iso_code(phone_country_code),
126-
"time_zone": tenant_user.time_zone,
127-
"language": tenant_user.language,
128-
"wx_userid": tenant_user.wx_userid,
129-
"domain": self.get_domain(tenant_user.data_source_id, tenant_user.tenant_id),
130-
"category_id": tenant_user.data_source_id,
131-
"status": TENANT_USER_STATUS_TO_PROFILE_STATUS_MAP.get(tenant_user.status, tenant_user.status),
132-
"enabled": True,
133-
"extras": extras,
134-
# 旧版本内置字段,新版本迁移在自定义字段里
135-
"position": int(source_extras.get("position", 0)),
136-
# 总是返回固定值
137-
"logo": "",
138-
"staff_status": "IN",
139-
}
161+
return {
162+
"leader_map": self._get_leader_map(data_source_user_ids) if "leader" in fields else {},
163+
"department_map": self._get_department_map(data_source_user_ids) if "departments" in fields else {},
164+
"collaboration_field_mapping": (
165+
self.get_collaboration_field_mapping() if "extras" in fields or "position" in fields else {}
166+
),
167+
}
140168

141-
# 指定对外字段,则只返回指定的字段
142-
if fields:
143-
user_info = {k: v for k, v in user_info.items() if k in fields}
144-
# 由于 leader 需要额外计算,因此特殊分支处理
145-
if "leader" in fields:
146-
user_info["leader"] = leader_map.get((tenant_user.tenant.id, tenant_user.data_source_user.id))
147-
# 由于 department 需要额外计算,因此特殊分支处理
148-
if "departments" in fields:
149-
user_info["departments"] = department_map.get(
150-
(tenant_user.tenant.id, tenant_user.data_source_user.id)
151-
)
169+
def _build_single_user_info(
170+
self, tenant_user: TenantUser, fields: List[str], context: Dict[str, Any]
171+
) -> Dict[str, Any]:
172+
"""构建单个用户的信息"""
173+
user_info: Dict[str, Any] = {}
152174

153-
user_infos.append(user_info)
154-
continue
175+
# 添加基础字段
176+
self._add_basic_fields(user_info, tenant_user, fields)
155177

156-
# 未指定字段,则关联字段也要返回
157-
user_info["leader"] = leader_map.get((tenant_user.tenant.id, tenant_user.data_source_user.id))
158-
user_info["departments"] = department_map.get((tenant_user.tenant.id, tenant_user.data_source_user.id))
178+
# 添加电话相关字段
179+
self._add_phone_fields(user_info, tenant_user, fields)
159180

160-
user_infos.append(user_info)
181+
# 添加简单字段
182+
self._add_simple_fields(user_info, tenant_user, fields)
183+
184+
# 添加自定义字段
185+
self._add_extras_fields(user_info, tenant_user, fields, context["collaboration_field_mapping"])
186+
187+
# 添加固定值字段
188+
user_info["logo"] = ""
189+
user_info["staff_status"] = "IN"
190+
191+
# 添加关联字段
192+
self._add_relation_fields(user_info, tenant_user, fields, context)
193+
194+
return user_info
161195

162-
return user_infos
196+
@staticmethod
197+
def _add_basic_fields(user_info: Dict[str, Any], tenant_user: TenantUser, fields: List[str]) -> None:
198+
"""添加基础字段"""
199+
if "id" in fields:
200+
user_info["id"] = tenant_user.data_source_user.id
201+
if "username" in fields:
202+
user_info["username"] = tenant_user.id
203+
if "display_name" in fields:
204+
user_info["display_name"] = tenant_user.data_source_user.full_name
205+
if "email" in fields:
206+
user_info["email"] = tenant_user.email
207+
208+
@staticmethod
209+
def _add_phone_fields(user_info: Dict[str, Any], tenant_user: TenantUser, fields: List[str]) -> None:
210+
"""添加电话相关字段"""
211+
if not any(f in fields for f in ["telephone", "country_code", "iso_code"]):
212+
return
213+
214+
phone, phone_country_code = tenant_user.phone_info
215+
if "telephone" in fields:
216+
user_info["telephone"] = phone
217+
if "country_code" in fields:
218+
user_info["country_code"] = phone_country_code
219+
if "iso_code" in fields:
220+
user_info["iso_code"] = _phone_country_code_to_iso_code(phone_country_code)
221+
222+
def _add_simple_fields(self, user_info: Dict[str, Any], tenant_user: TenantUser, fields: List[str]) -> None:
223+
"""添加简单字段"""
224+
simple_field_mapping = {
225+
"time_zone": lambda: tenant_user.time_zone,
226+
"language": lambda: tenant_user.language,
227+
"wx_userid": lambda: tenant_user.wx_userid,
228+
"domain": lambda: self.get_domain(tenant_user.data_source_id, tenant_user.tenant_id),
229+
"category_id": lambda: tenant_user.data_source_id,
230+
"status": lambda: TENANT_USER_STATUS_TO_PROFILE_STATUS_MAP.get(tenant_user.status, tenant_user.status),
231+
}
232+
233+
for field_name, value_func in simple_field_mapping.items():
234+
if field_name in fields:
235+
user_info[field_name] = value_func()
236+
237+
@staticmethod
238+
def _add_extras_fields(
239+
user_info: Dict[str, Any],
240+
tenant_user: TenantUser,
241+
fields: List[str],
242+
collaboration_field_mapping: Dict[Tuple[str, str], str],
243+
) -> None:
244+
"""添加自定义字段"""
245+
if not ("extras" in fields or "position" in fields):
246+
return
247+
248+
source_extras = tenant_user.data_source_user.extras
249+
extras = TenantUserListToUserInfosMixin._get_collaboration_extras(
250+
source_extras, tenant_user, collaboration_field_mapping
251+
)
252+
253+
if "extras" in fields:
254+
user_info["extras"] = extras
255+
if "position" in fields:
256+
user_info["position"] = int(source_extras.get("position", 0))
257+
258+
@staticmethod
259+
def _get_collaboration_extras(
260+
source_extras: Dict[str, Any],
261+
tenant_user: TenantUser,
262+
collaboration_field_mapping: Dict[Tuple[str, str], str],
263+
) -> Dict[str, Any]:
264+
"""获取协同场景下的自定义字段"""
265+
ds_owner_tenant_id = tenant_user.data_source.owner_tenant_id
266+
if ds_owner_tenant_id != tenant_user.tenant_id:
267+
return {
268+
collaboration_field_mapping[(ds_owner_tenant_id, k)]: v
269+
for k, v in source_extras.items()
270+
if (ds_owner_tenant_id, k) in collaboration_field_mapping
271+
}
272+
return source_extras
273+
274+
@staticmethod
275+
def _add_relation_fields(
276+
user_info: Dict[str, Any], tenant_user: TenantUser, fields: List[str], context: Dict[str, Any]
277+
) -> None:
278+
"""添加关联字段"""
279+
if "leader" in fields:
280+
user_info["leader"] = context["leader_map"].get((tenant_user.tenant.id, tenant_user.data_source_user.id))
281+
if "departments" in fields:
282+
user_info["departments"] = context["department_map"].get(
283+
(tenant_user.tenant.id, tenant_user.data_source_user.id)
284+
)
163285

164286
@staticmethod
165287
def _get_leader_map(data_source_user_ids: List[int]) -> Dict[Tuple[str, int], List[Dict[str, Any]]]:
@@ -480,7 +602,7 @@ def get(self, request, *args, **kwargs):
480602
slz.is_valid(raise_exception=True)
481603
params = slz.validated_data
482604

483-
lookup_filter = {}
605+
lookup_filter: Dict[str, Any] = {}
484606
if params["lookup_field"] == "username":
485607
# username 其实就是新的租户用户 ID,形式如 admin / admin@qq.com / uuid4 / nanoid
486608
lookup_filter["id"] = kwargs["lookup_value"]

src/bk-user/bkuser/common/cache.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import functools
1919

2020
from blue_krill.data_types.enum import EnumField, StrStructuredEnum
21-
from django.core.cache import cache as default_cache
2221
from django.core.cache import caches
2322
from django.core.cache.backends.base import DEFAULT_TIMEOUT
2423

@@ -73,16 +72,16 @@ def _method_key_function(_, *args, **kwargs):
7372
# cached 和 cachedmethod 其 key 的生成方法可以满足大部分情况下不冲突,但有以下几种情况可能会冲突
7473
# (1) 对于类的实例方法,由于缓存 key 只用到方法的自定义参数,
7574
# 若 key 的区分需要用到 self.{attr},则需要重新自定义,否则相同方法参数时会冲突
76-
# (2) 虽然模块名+方法名作为了 key 的前缀,但由于是字符串拼接,
75+
# (2) 虽然模块名 + 方法名作为了 key 的前缀,但由于是字符串拼接,
7776
# 有极少概率会出现拼接出来的结果一样的情况而导致冲突
7877
# (3) key 的字符串拼接,若参数里的值包含分隔符 "|",有可能出现冲突
7978
# (4) 生成 key 时做了字符串转换,对于某些对象可能 str() 后相同,
8079
# 建议参数类型为:str/bool/int/tuple/List[base_type]/Dict[base_type]
81-
def cached(cache=default_cache, key_function=_default_key_function, timeout=DEFAULT_TIMEOUT):
80+
def cached(cache_name=CacheEnum.DEFAULT.value, key_function=_default_key_function, timeout=DEFAULT_TIMEOUT):
8281
"""Decorator to wrap a function with a memorizing callable that saves results in a cache.
8382
cache param usage:
8483
from django.core.cache import caches
85-
@cached(cache=caches[CacheEnum.REDIS], ...)
84+
@cached(cache_name=CacheEnum.REDIS, ...)
8685
"""
8786

8887
def decorator(func):
@@ -92,14 +91,14 @@ def wrapper(*args, **kwargs):
9291
namespace = f"{func.__module__}:{func.__name__}"
9392
key = f"{CacheKeyPrefixEnum.AUTO}:{namespace}:{custom_key}"
9493

95-
return cache.get_or_set(key, lambda: func(*args, **kwargs), timeout)
94+
return caches[str(cache_name)].get_or_set(key, lambda: func(*args, **kwargs), timeout)
9695

9796
return wrapper
9897

9998
return decorator
10099

101100

102-
def cachedmethod(cache=default_cache, key_function=_method_key_function, timeout=DEFAULT_TIMEOUT):
101+
def cachedmethod(cache_name=CacheEnum.DEFAULT.value, key_function=_method_key_function, timeout=DEFAULT_TIMEOUT):
103102
"""Decorator to wrap a class or instance method with a memorizing
104103
callable that saves results in a cache.
105104
"""
@@ -111,7 +110,7 @@ def wrapper(self, *args, **kwargs):
111110
namespace = f"{method.__module__}:{method.__qualname__}"
112111
key = f"{CacheKeyPrefixEnum.AUTO}:{namespace}:{custom_key}"
113112

114-
return cache.get_or_set(key, lambda: method(self, *args, **kwargs), timeout)
113+
return caches[str(cache_name)].get_or_set(key, lambda: method(self, *args, **kwargs), timeout)
115114

116115
return wrapper
117116

@@ -122,7 +121,7 @@ class Cache:
122121
"""
123122
Cache 用于避免直接使用 Django Caches 时导致不同场景的前缀 Key 冲突问题,
124123
使用各个场景更专注于自身业务逻辑缓存和 key 生成,Cache 所有方法都基于
125-
Django Cache 的 BaseCache ,只封装了项目所需方法
124+
Django Cache 的 BaseCache,只封装了项目所需方法
126125
"""
127126

128127
def __init__(self, type_, key_prefix):

src/bk-user/tests/apis/open_v2/conftest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from bkuser.apps.tenant.models import CollaborationStrategy
2222
from bkuser.plugins.general.models import GeneralDataSourcePluginConfig
2323
from bkuser.plugins.local.models import LocalDataSourcePluginConfig
24-
from django.core.cache import cache
24+
from django.core.cache import caches
2525

2626
from tests.test_utils.data_source import init_data_source_users_depts_and_relations
2727
from tests.test_utils.helpers import generate_random_string
@@ -33,9 +33,12 @@
3333
@pytest.fixture(autouse=True)
3434
def _clear_cache():
3535
"""在每个测试前清除缓存,避免缓存的方法返回值影响测试结果"""
36-
cache.clear()
36+
# 清除所有配置的缓存
37+
for cache_name in caches:
38+
caches[cache_name].clear()
3739
yield
38-
cache.clear()
40+
for cache_name in caches:
41+
caches[cache_name].clear()
3942

4043

4144
@pytest.fixture

0 commit comments

Comments
 (0)