diff --git a/src/bk-user/bkuser/apis/web/person_center/serializers.py b/src/bk-user/bkuser/apis/web/person_center/serializers.py index 670e5f5cb..0bdd632c0 100644 --- a/src/bk-user/bkuser/apis/web/person_center/serializers.py +++ b/src/bk-user/bkuser/apis/web/person_center/serializers.py @@ -17,6 +17,8 @@ from bkuser.apis.web.organization.serializers import TenantUserDepartmentOutputSLZ, TenantUserLeaderOutputSLZ from bkuser.apps.tenant.models import TenantUser from bkuser.biz.tenant import TenantUserHandler +from bkuser.common.error_codes import error_codes +from bkuser.common.validators import validate_phone_with_country_code class TenantBaseInfoOutputSLZ(serializers.Serializer): @@ -97,3 +99,34 @@ def to_representation(self, instance: TenantUser) -> Dict: } ) return data + + +class TenantUserPhoneInputSLZ(serializers.Serializer): + is_inherited_phone = serializers.BooleanField(required=True, help_text="是否继承数据源手机号") + custom_phone = serializers.CharField(required=False, help_text="自定义用户手机号") + custom_phone_country_code = serializers.CharField( + required=False, help_text="自定义用户手机国际区号", default=settings.DEFAULT_PHONE_COUNTRY_CODE + ) + + def validate(self, attrs): + # custom_phone_country_code 默认为:86 + # 通过继承,custom_phone 必须存在 + if not attrs["is_inherited_phone"] and not attrs.get("custom_phone"): + raise error_codes.VALIDATION_ERROR.f("缺少参数:custom_phone") + + # 校验手机号 + validate_phone_with_country_code(phone=attrs["custom_phone"], country_code=attrs["custom_phone_country_code"]) + + return attrs + + +class TenantUserEmailInputSLZ(serializers.Serializer): + is_inherited_email = serializers.BooleanField(required=True, help_text="是否继承数据源邮箱") + custom_email = serializers.EmailField(required=False, help_text="自定义用户邮箱") + + def validate(self, attrs): + # 通过继承,custom_email 必须存在 + if not attrs["is_inherited_email"] and not attrs.get("custom_email"): + raise error_codes.VALIDATION_ERROR.f("缺少参数:custom_email") + + return attrs diff --git a/src/bk-user/bkuser/apis/web/person_center/urls.py b/src/bk-user/bkuser/apis/web/person_center/urls.py index 5acb14e08..28ba52ccb 100644 --- a/src/bk-user/bkuser/apis/web/person_center/urls.py +++ b/src/bk-user/bkuser/apis/web/person_center/urls.py @@ -21,4 +21,14 @@ ), # 租户用户详情 path("tenant-users//", views.TenantUserRetrieveApi.as_view(), name="person_center.tenant_users.retrieve"), + path( + "tenant-users//phone/", + views.TenantUserPhonePatchApi.as_view(), + name="person_center.tenant_users.phone.patch", + ), + path( + "tenant-users//email/", + views.TenantUserEmailPatchApi.as_view(), + name="person_center.tenant_users.email.patch", + ), ] diff --git a/src/bk-user/bkuser/apis/web/person_center/views.py b/src/bk-user/bkuser/apis/web/person_center/views.py index 31d279302..1ca933e53 100644 --- a/src/bk-user/bkuser/apis/web/person_center/views.py +++ b/src/bk-user/bkuser/apis/web/person_center/views.py @@ -8,7 +8,6 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ -from typing import List from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status @@ -17,10 +16,13 @@ from bkuser.apis.web.mixins import CurrentUserTenantMixin from bkuser.apis.web.person_center.serializers import ( NaturalUserWithTenantUserListOutputSLZ, + TenantUserEmailInputSLZ, + TenantUserPhoneInputSLZ, TenantUserRetrieveOutputSLZ, ) from bkuser.apps.tenant.models import TenantUser from bkuser.biz.natural_user import NaturalUserWithTenantUsers, NatureUserHandler, TenantBaseInfo, TenantUserBaseInfo +from bkuser.biz.tenant import TenantUserHandler, TenantUserUpdateEmailInfo, TenantUserUpdatePhoneInfo from bkuser.common.error_codes import error_codes @@ -33,30 +35,23 @@ class NaturalUserTenantUserListApi(CurrentUserTenantMixin, generics.ListAPIView) responses={status.HTTP_200_OK: NaturalUserWithTenantUserListOutputSLZ()}, ) def get(self, request, *args, **kwargs): - current_tenant_user = self.get_current_tenant_user() - # 获取当前登录的租户用户的自然人 + # 未绑定自然人,则返回(伪)自然人=>租户用户的对应信息 + current_tenant_user = self.get_current_tenant_user() nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(current_tenant_user.id) - data_source_user_ids: List[int] = [] - - # 当前租户用户的数据源用户绑定了自然人,返回自然人绑定数据源用户 - if nature_user is not None: - data_source_user_ids += nature_user.data_source_user_ids - else: - # 未绑定自然人,则返回当用户所属数据源用户 - data_source_user_ids.append(current_tenant_user.data_source_user.id) - # 将当前登录置顶 # 通过比对租户用户id, 当等于当前登录用户的租户id,将其排序到查询集的顶部, 否则排序到查询集的底部 - tenant_users = TenantUser.objects.filter(data_source_user_id__in=data_source_user_ids) + tenant_users = TenantUser.objects.select_related("data_source_user").filter( + data_source_user_id__in=nature_user.data_source_user_ids + ) sorted_tenant_users = sorted(tenant_users, key=lambda t: t.id != current_tenant_user.id) # 响应数据组装 # 当前登录的用户,未绑定自然人,(伪)自然人为当前租户用户 data = NaturalUserWithTenantUsers( - id=nature_user.id if nature_user else current_tenant_user.id, - full_name=nature_user.full_name if nature_user else current_tenant_user.data_source_user.full_name, + id=nature_user.id, + full_name=nature_user.full_name, tenant_users=[ TenantUserBaseInfo( id=user.id, @@ -82,22 +77,80 @@ class TenantUserRetrieveApi(CurrentUserTenantMixin, generics.RetrieveAPIView): responses={status.HTTP_200_OK: TenantUserRetrieveOutputSLZ()}, ) def get(self, request, *args, **kwargs): + instance: TenantUser = self.get_object() + + # 获取当前登录的租户用户的自然人 + # 未绑定自然人,则返回(伪)自然人=>租户用户的对应信息 current_tenant_user = self.get_current_tenant_user() + nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(current_tenant_user.id) + + # 边界限制 + # 该租户用户的数据源用户,不属于当前自然人 + if instance.data_source_user_id not in nature_user.data_source_user_ids: + raise error_codes.NO_PERMISSION + + return Response(TenantUserRetrieveOutputSLZ(instance).data) + + +class TenantUserPhonePatchApi(CurrentUserTenantMixin, generics.UpdateAPIView): + queryset = TenantUser.objects.all() + lookup_url_kwarg = "id" + + @swagger_auto_schema( + tags=["person_center"], + operation_description="租户用户更新手机号", + request_body=TenantUserPhoneInputSLZ, + responses={status.HTTP_200_OK: ""}, + ) + def patch(self, request, *args, **kwargs): + instance: TenantUser = self.get_object() # 获取当前登录的租户用户的自然人 + # 未绑定自然人,则返回(伪)自然人=>租户用户的对应信息 + current_tenant_user = self.get_current_tenant_user() nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(current_tenant_user.id) - data_source_user_ids: List[int] = [] + # 边界限制 + # 该租户用户的数据源用户,不属于当前自然人 + if instance.data_source_user_id not in nature_user.data_source_user_ids: + raise error_codes.NO_PERMISSION + + input_slz = TenantUserPhoneInputSLZ(data=request.data) + input_slz.is_valid(raise_exception=True) + + phone_info = TenantUserUpdatePhoneInfo(**input_slz.validated_data) + TenantUserHandler.update_tenant_user_phone(instance, phone_info) - # 当前租户用户的数据源用户绑定了自然人,返回自然人绑定数据源用户 - if nature_user is not None: - data_source_user_ids += nature_user.data_source_user_ids - else: - # 未绑定自然人,则返回当用户所属数据源用户 - data_source_user_ids.append(current_tenant_user.data_source_user.id) + return Response() + +class TenantUserEmailPatchApi(CurrentUserTenantMixin, generics.UpdateAPIView): + queryset = TenantUser.objects.all() + lookup_url_kwarg = "id" + + @swagger_auto_schema( + tags=["person_center"], + operation_description="租户用户更新手机号", + request_body=TenantUserEmailInputSLZ, + responses={status.HTTP_200_OK: ""}, + ) + def patch(self, request, *args, **kwargs): instance: TenantUser = self.get_object() - if instance.data_source_user_id not in data_source_user_ids: + + # 获取当前登录的租户用户的自然人 + # 未绑定自然人,则返回(伪)自然人=>租户用户的对应信息 + current_tenant_user = self.get_current_tenant_user() + nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(current_tenant_user.id) + + # 边界限制 + # 该租户用户的数据源用户,不属于当前自然人 + if instance.data_source_user_id not in nature_user.data_source_user_ids: raise error_codes.NO_PERMISSION - return Response(TenantUserRetrieveOutputSLZ(instance).data) + input_slz = TenantUserEmailInputSLZ(data=request.data) + input_slz.is_valid(raise_exception=True) + + email_info = TenantUserUpdateEmailInfo(**input_slz.validated_data) + TenantUserHandler.update_tenant_user_email(instance, email_info) + + return Response() diff --git a/src/bk-user/bkuser/biz/natural_user.py b/src/bk-user/bkuser/biz/natural_user.py index 5081575e4..f2ad5ca03 100644 --- a/src/bk-user/bkuser/biz/natural_user.py +++ b/src/bk-user/bkuser/biz/natural_user.py @@ -8,7 +8,7 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ -from typing import List, Optional +from typing import List from pydantic import BaseModel @@ -43,7 +43,7 @@ class NaturalUserWithTenantUsers(BaseModel): class NatureUserHandler: @staticmethod - def get_nature_user_by_tenant_user_id(tenant_user_id: str) -> Optional[NaturalUserInfo]: + def get_nature_user_by_tenant_user_id(tenant_user_id: str) -> NaturalUserInfo: """ 通过租户用户ID获取对应的自然人ID """ @@ -55,10 +55,15 @@ def get_nature_user_by_tenant_user_id(tenant_user_id: str) -> Optional[NaturalUs data_source_user_id=tenant_user.data_source_user_id ).first() if not natural_user_relation: - return None + # 未绑定自然人,则返回(伪)自然人=>租户用户对应信息 + return NaturalUserInfo( + id=tenant_user.id, + full_name=tenant_user.data_source_user.full_name, + data_source_user_ids=[tenant_user.data_source_user_id], + ) + # 未绑定自然人,则返回自然人信息 natural_user = natural_user_relation.natural_user - return NaturalUserInfo( id=natural_user.id, full_name=natural_user.full_name, diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py index 33c0824e2..285e14722 100644 --- a/src/bk-user/bkuser/biz/tenant.py +++ b/src/bk-user/bkuser/biz/tenant.py @@ -11,6 +11,7 @@ from collections import defaultdict from typing import Dict, List, Optional +from django.conf import settings from django.db import transaction from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -90,6 +91,17 @@ class TenantUserLeaderInfo(BaseModel): full_name: str +class TenantUserUpdatePhoneInfo(BaseModel): + is_inherited_phone: bool + custom_phone: Optional[str] = "" + custom_phone_country_code: Optional[str] = settings.DEFAULT_PHONE_COUNTRY_CODE + + +class TenantUserUpdateEmailInfo(BaseModel): + is_inherited_email: bool + custom_email: Optional[str] = "" + + class TenantUserHandler: @staticmethod def list_tenant_user_by_id(tenant_user_ids: List[str]) -> List[TenantUserWithInheritedInfo]: @@ -222,6 +234,21 @@ def get_tenant_user_ids_by_tenant(tenant_id: str) -> List[str]: "id", flat=True ) + @staticmethod + def update_tenant_user_phone(tenant_user: TenantUser, phone_info: TenantUserUpdatePhoneInfo): + tenant_user.is_inherited_phone = phone_info.is_inherited_phone + if not phone_info.is_inherited_phone: + tenant_user.custom_phone = phone_info.custom_phone + tenant_user.custom_phone_country_code = phone_info.custom_phone_country_code + tenant_user.save() + + @staticmethod + def update_tenant_user_email(tenant_user: TenantUser, email_info: TenantUserUpdateEmailInfo): + tenant_user.is_inherited_email = email_info.is_inherited_email + if not email_info.is_inherited_email: + tenant_user.custom_email = email_info.custom_email + tenant_user.save() + class TenantHandler: @staticmethod