diff --git a/apps/common/util/field_message.py b/apps/common/util/field_message.py index 3d0fe9d46..fd961dc8a 100644 --- a/apps/common/util/field_message.py +++ b/apps/common/util/field_message.py @@ -17,8 +17,8 @@ class ErrMessage: 'blank': gettext_lazy("【%s】此字段不能为空字符串。" % field), 'max_length': gettext_lazy("【%s】请确保此字段的字符数不超过 {max_length} 个。" % field), 'min_length': gettext_lazy("【%s】请确保此字段至少包含 {min_length} 个字符。" % field), - 'required': gettext_lazy('此字段必填。'), - 'null': gettext_lazy('此字段不能为null。') + 'required': gettext_lazy('【%s】此字段必填。' % field), + 'null': gettext_lazy('【%s】此字段不能为null。' % field) } @staticmethod diff --git a/apps/setting/serializers/team_serializers.py b/apps/setting/serializers/team_serializers.py index 9a39f70ef..46266bb35 100644 --- a/apps/setting/serializers/team_serializers.py +++ b/apps/setting/serializers/team_serializers.py @@ -25,7 +25,7 @@ from common.mixins.api_mixin import ApiMixin from common.response.result import get_api_response from common.util.field_message import ErrMessage from common.util.file_util import get_file_content -from setting.models import TeamMember, TeamMemberPermission +from setting.models import TeamMember, TeamMemberPermission, Team from smartdoc.conf import PROJECT_DIR from users.models.user import User from users.serializers.user_serializers import UserSerializer diff --git a/apps/users/migrations/0002_user_nick_name_user_phone.py b/apps/users/migrations/0002_user_nick_name_user_phone.py new file mode 100644 index 000000000..ae449fc3c --- /dev/null +++ b/apps/users/migrations/0002_user_nick_name_user_phone.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.10 on 2024-03-18 13:55 +import uuid + +from django.db import migrations, models + +from common.constants.permission_constants import RoleConstants +from users.models import password_encrypt + + +def insert_default_data(apps, schema_editor): + UserModel = apps.get_model('users', 'User') + UserModel.objects.create(id='f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', email='', username='admin', nick_name="管理员", + password=password_encrypt('MaxKB@123..'), + role=RoleConstants.ADMIN.name, + is_active=True) + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='nick_name', + field=models.CharField(default='', max_length=150, verbose_name='昵称'), + ), + migrations.AddField( + model_name='user', + name='phone', + field=models.CharField(default='', max_length=20, verbose_name='电话'), + ), + migrations.RunPython(insert_default_data) + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 3eb015a37..0b9184e2b 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -63,6 +63,8 @@ def get_user_dynamics_permission(user_id: str): class User(models.Model): id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id") email = models.EmailField(unique=True, verbose_name="邮箱") + phone = models.CharField(max_length=20, verbose_name="电话", default="") + nick_name = models.CharField(max_length=150, verbose_name="昵称", default="") username = models.CharField(max_length=150, unique=True, verbose_name="用户名") password = models.CharField(max_length=150, verbose_name="密码") role = models.CharField(max_length=150, verbose_name="角色") diff --git a/apps/users/serializers/user_serializers.py b/apps/users/serializers/user_serializers.py index 33d9ed7fd..0302e6b00 100644 --- a/apps/users/serializers/user_serializers.py +++ b/apps/users/serializers/user_serializers.py @@ -22,6 +22,7 @@ from rest_framework import serializers from common.constants.authentication_type import AuthenticationType from common.constants.exception_code_constants import ExceptionCodeConstants from common.constants.permission_constants import RoleConstants, get_permission_list_by_role +from common.db.search import page_search from common.exception.app_exception import AppApiException from common.mixins.api_mixin import ApiMixin from common.response.result import get_api_response @@ -50,9 +51,9 @@ class LoginSerializer(ApiMixin, serializers.Serializer): super().is_valid(raise_exception=True) username = self.data.get("username") password = password_encrypt(self.data.get("password")) - user = self.Meta.model.objects.filter(Q(username=username, - password=password) | Q(email=username, - password=password)).first() + user = QuerySet(User).filter(Q(username=username, + password=password) | Q(email=username, + password=password)).first() if user is None: raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception() return user @@ -142,7 +143,7 @@ class RegisterSerializer(ApiMixin, serializers.Serializer): cache_code = user_cache.get(code_cache_key) if code != cache_code: raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() - u = User.objects.filter(Q(username=username) | Q(email=email)).first() + u = QuerySet(User).filter(Q(username=username) | Q(email=email)).first() if u is not None: if u.email == email: raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception() @@ -274,7 +275,7 @@ class RePasswordSerializer(ApiMixin, serializers.Serializer): """ if self.is_valid(): email = self.data.get("email") - self.Meta.model.objects.filter(email=email).update( + QuerySet(User).filter(email=email).update( password=password_encrypt(self.data.get('password'))) code_cache_key = email + ":reset_password" # 删除验证码缓存 @@ -312,7 +313,7 @@ class SendEmailSerializer(ApiMixin, serializers.Serializer): def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=raise_exception) - user_exists = self.Meta.model.objects.filter(email=self.data.get('email')).exists() + user_exists = QuerySet(User).filter(email=self.data.get('email')).exists() if not user_exists and self.data.get('type') == 'reset_password': raise ExceptionCodeConstants.EMAIL_IS_NOT_EXIST.value.to_app_api_exception() elif user_exists and self.data.get('type') == 'register': @@ -450,3 +451,255 @@ class UserSerializer(ApiMixin, serializers.ModelSerializer): email_or_username = self.data.get('email_or_username') return [{'id': user_model.id, 'username': user_model.username, 'email': user_model.email} for user_model in QuerySet(User).filter(Q(username=email_or_username) | Q(email=email_or_username))] + + +class UserInstanceSerializer(ApiMixin, serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'email', 'phone', 'nick_name'] + + @staticmethod + def get_response_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['id', 'username', 'email', 'password', 'create_time', 'update_time'], + properties={ + 'username': openapi.Schema(type=openapi.TYPE_STRING, title="用户名", description="用户名"), + 'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"), + 'phone': openapi.Schema(type=openapi.TYPE_STRING, title="手机号", description="手机号"), + 'nick_name': openapi.Schema(type=openapi.TYPE_STRING, title="昵称", description="昵称"), + 'create_time': openapi.Schema(type=openapi.TYPE_STRING, title="创建时间", description="修改时间"), + 'update_time': openapi.Schema(type=openapi.TYPE_STRING, title="修改时间", description="修改时间") + } + ) + + @staticmethod + def get_request_params_api(): + return [openapi.Parameter(name='user_id', + in_=openapi.IN_PATH, + type=openapi.TYPE_STRING, + required=True, + description='用户名id') + + ] + + +class UserManageSerializer(serializers.Serializer): + class Query(ApiMixin, serializers.Serializer): + email_or_username = serializers.CharField(required=False, error_messages=ErrMessage.char("邮箱或者用户名")) + + @staticmethod + def get_request_params_api(): + return [openapi.Parameter(name='email_or_username', + in_=openapi.IN_QUERY, + type=openapi.TYPE_STRING, + required=False, + description='邮箱或者用户名')] + + @staticmethod + def get_response_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['username', 'email', 'id'], + properties={ + 'id': openapi.Schema(type=openapi.TYPE_STRING, title='用户主键id', description="用户主键id"), + 'username': openapi.Schema(type=openapi.TYPE_STRING, title="用户名", description="用户名"), + 'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址") + } + ) + + def get_query_set(self): + email_or_username = self.data.get('email_or_username') + query_set = QuerySet(User) + if email_or_username is not None: + query_set = query_set.filter(Q(username=email_or_username) | Q(email=email_or_username)) + return query_set + + def list(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return [{'id': user_model.id, 'username': user_model.username, 'email': user_model.email} for user_model in + self.get_query_set()] + + def page(self, current_page: int, page_size: int, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return page_search(current_page, page_size, + self.get_query_set(), + post_records_handler=lambda u: UserInstanceSerializer(u).data) + + class UserInstance(ApiMixin, serializers.Serializer): + email = serializers.EmailField( + required=True, + error_messages=ErrMessage.char("邮箱"), + validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message, + code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)]) + + username = serializers.CharField(required=True, + error_messages=ErrMessage.char("用户名"), + max_length=20, + min_length=6, + validators=[ + validators.RegexValidator(regex=re.compile("^[a-zA-Z][a-zA-Z1-9_]{5,20}$"), + message="用户名字符数为 6-20 个字符,必须以字母开头,可使用字母、数字、下划线等") + ]) + password = serializers.CharField(required=True, error_messages=ErrMessage.char("密码"), + validators=[validators.RegexValidator(regex=re.compile( + "^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)" + "(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~()-+=]{6,20}$") + , message="密码长度6-20个字符,必须字母、数字、特殊字符组合")]) + + nick_name = serializers.CharField(required=False, error_messages=ErrMessage.char("昵称"), max_length=56, + allow_null=True) + phone = serializers.CharField(required=False, error_messages=ErrMessage.char("手机号"), max_length=20, + allow_null=True) + + def is_valid(self, *, raise_exception=True): + super().is_valid(raise_exception=True) + username = self.data.get('username') + email = self.data.get('email') + u = QuerySet(User).filter(Q(username=username) | Q(email=email)).first() + if u is not None: + if u.email == email: + raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception() + if u.username == username: + raise ExceptionCodeConstants.USERNAME_IS_EXIST.value.to_app_api_exception() + + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['username', 'email', 'password'], + properties={ + 'username': openapi.Schema(type=openapi.TYPE_STRING, title="用户名", description="用户名"), + 'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"), + 'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码"), + 'phone': openapi.Schema(type=openapi.TYPE_STRING, title="手机号", description="手机号"), + 'nick_name': openapi.Schema(type=openapi.TYPE_STRING, title="昵称", description="昵称") + } + ) + + class UserEditInstance(ApiMixin, serializers.Serializer): + email = serializers.EmailField( + required=False, + error_messages=ErrMessage.char("邮箱"), + validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message, + code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)]) + + nick_name = serializers.CharField(required=False, error_messages=ErrMessage.char("昵称"), max_length=56, + allow_null=True) + phone = serializers.CharField(required=False, error_messages=ErrMessage.char("手机号"), max_length=20, + allow_null=True) + is_active = serializers.BooleanField(required=False, error_messages=ErrMessage.char("是否可用")) + + def is_valid(self, *, user_id=None, raise_exception=False): + super().is_valid(raise_exception=True) + if QuerySet(User).filter(email=self.data.get('email')).exclude(id=user_id).exists(): + raise AppApiException(1004, "邮箱已经被使用") + + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱"), + 'nick_name': openapi.Schema(type=openapi.TYPE_STRING, title="昵称", description="昵称"), + 'phone': openapi.Schema(type=openapi.TYPE_STRING, title="手机号", description="手机号"), + 'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title="是否可用", description="是否可用"), + } + ) + + class RePasswordInstance(ApiMixin, serializers.Serializer): + password = serializers.CharField(required=True, error_messages=ErrMessage.char("密码"), + validators=[validators.RegexValidator(regex=re.compile( + "^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)" + "(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~()-+=]{6,20}$") + , message="密码长度6-20个字符,必须字母、数字、特殊字符组合")]) + re_password = serializers.CharField(required=True, error_messages=ErrMessage.char("确认密码"), + validators=[validators.RegexValidator(regex=re.compile( + "^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z_!@#$%^&*`~()-+=]+$)" + "(?![0-9_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9_!@#$%^&*`~()-+=]{6,20}$") + , message="确认密码长度6-20个字符,必须字母、数字、特殊字符组合")] + ) + + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['password', 're_password'], + properties={ + 'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码"), + 're_password': openapi.Schema(type=openapi.TYPE_STRING, title="确认密码", + description="确认密码"), + } + ) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if self.data.get('password') != self.data.get('re_password'): + raise ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.to_app_api_exception() + + @transaction.atomic + def save(self, instance, with_valid=True): + if with_valid: + UserManageSerializer.UserInstance(data=instance).is_valid(raise_exception=True) + + user = User(id=uuid.uuid1(), email=instance.get('email'), + phone="" if instance.get('phone') is None else instance.get('phone'), + nick_name="" if instance.get('nick_name') is None else instance.get('nick_name') + , username=instance.get('username'), password=password_encrypt(instance.get('password')), + role=RoleConstants.USER.name, + is_active=True) + user.save() + # 初始化用户团队 + Team(**{'user': user, 'name': user.username + '的团队'}).save() + return UserInstanceSerializer(user).data + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True, error_messages=ErrMessage.char("用户id")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if not QuerySet(User).filter(id=self.data.get('id')).exists(): + raise AppApiException(1004, "用户不存在") + + def delete(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user = QuerySet(User).filter(id=self.data.get('id')).first() + if user.role == RoleConstants.ADMIN.name: + raise AppApiException(1004, "无法删除管理员") + QuerySet(User).filter(id=self.data.get('id')).delete() + return True + + def edit(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + UserManageSerializer.UserEditInstance(data=instance).is_valid(user_id=self.data.get('id'), + raise_exception=True) + + user = QuerySet(User).filter(id=self.data.get('id')).first() + if user.role == RoleConstants.ADMIN.name and 'is_active' in instance and instance.get( + 'is_active') is not None: + raise AppApiException(1004, "不能修改管理员状态") + update_keys = ['email', 'nick_name', 'phone', 'is_active'] + for update_key in update_keys: + if update_key in instance and instance.get(update_key) is not None: + user.__setattr__(update_key, instance.get(update_key)) + user.save() + return UserInstanceSerializer(user).data + + def one(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + user = QuerySet(User).filter(id=self.data.get('id')).first() + return UserInstanceSerializer(user).data + + def re_password(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + UserManageSerializer.RePasswordInstance(data=instance).is_valid(raise_exception=True) + user = QuerySet(User).filter(id=self.data.get('id')).first() + user.password = password_encrypt(instance.get('password')) + user.save() + return True diff --git a/apps/users/urls.py b/apps/users/urls.py index 908c01444..211acdd85 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -13,5 +13,11 @@ urlpatterns = [ path("user/check_code", views.CheckCode.as_view(), name='check_code'), path("user/re_password", views.RePasswordView.as_view(), name='re_password'), path("user/current/send_email", views.SendEmailToCurrentUserView.as_view(), name="send_email_current"), - path("user/current/reset_password", views.ResetCurrentUserPasswordView.as_view(), name="reset_password_current") + path("user/current/reset_password", views.ResetCurrentUserPasswordView.as_view(), name="reset_password_current"), + path("user_manage", views.UserManage.as_view(), name="user_manage"), + path("user_manage/", views.UserManage.Operate.as_view(), name="user_manage_operate"), + path("user_manage//re_password", views.UserManage.RePassword.as_view(), + name="user_manage_re_password"), + path("user_manage//", views.UserManage.Page.as_view(), + name="user_manage_re_password"), ] diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 76226aaa4..aca7e2b85 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -17,12 +17,12 @@ from rest_framework.views import Request from common.auth.authenticate import TokenAuth from common.auth.authentication import has_permissions -from common.constants.permission_constants import PermissionConstants +from common.constants.permission_constants import PermissionConstants, CompareConstants, ViewPermission, RoleConstants from common.response import result from smartdoc.settings import JWT_AUTH from users.serializers.user_serializers import RegisterSerializer, LoginSerializer, CheckCodeSerializer, \ RePasswordSerializer, \ - SendEmailSerializer, UserProfile, UserSerializer + SendEmailSerializer, UserProfile, UserSerializer, UserManageSerializer, UserInstanceSerializer user_cache = cache.caches['user_cache'] token_cache = cache.caches['token_cache'] @@ -191,3 +191,105 @@ class SendEmail(APIView): serializer_obj = SendEmailSerializer(data=request.data) if serializer_obj.is_valid(raise_exception=True): return result.success(serializer_obj.send()) + + +class UserManage(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['POST'], detail=False) + @swagger_auto_schema(operation_summary="添加用户", + operation_id="添加用户", + request_body=UserManageSerializer.UserInstance.get_request_body_api(), + responses=result.get_api_response(UserInstanceSerializer.get_response_body_api()), + tags=["用户管理"] + ) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def post(self, request: Request): + return result.success(UserManageSerializer().save(request.data)) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['GET'], detail=False) + @swagger_auto_schema(operation_summary="获取用户分页列表", + operation_id="获取用户分页列表", + tags=["用户管理"], + manual_parameters=UserManageSerializer.Query.get_request_params_api(), + responses=result.get_page_api_response(UserInstanceSerializer.get_response_body_api()), + ) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def get(self, request: Request, current_page, page_size): + d = UserManageSerializer.Query( + data={'name': request.query_params.get('name', None), 'desc': request.query_params.get("desc", None), + 'user_id': str(request.user.id)}) + return result.success(d.page(current_page, page_size)) + + class RePassword(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['PUT'], detail=False) + @swagger_auto_schema(operation_summary="修改密码", + operation_id="修改密码", + manual_parameters=UserInstanceSerializer.get_request_params_api(), + request_body=UserManageSerializer.RePasswordInstance.get_request_body_api(), + responses=result.get_default_response(), + tags=["用户管理"]) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def put(self, request: Request, user_id): + return result.success( + UserManageSerializer.Operate(data={'id': user_id}).re_password(request.data, with_valid=True)) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['DELETE'], detail=False) + @swagger_auto_schema(operation_summary="删除用户", + operation_id="删除用户", + manual_parameters=UserInstanceSerializer.get_request_params_api(), + responses=result.get_default_response(), + tags=["用户管理"]) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def delete(self, request: Request, user_id): + return result.success(UserManageSerializer.Operate(data={'id': user_id}).delete(with_valid=True)) + + @action(methods=['GET'], detail=False) + @swagger_auto_schema(operation_summary="获取用户信息", + operation_id="获取用户信息", + manual_parameters=UserInstanceSerializer.get_request_params_api(), + responses=result.get_api_response(UserInstanceSerializer.get_response_body_api()), + tags=["用户管理"] + ) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def get(self, request: Request, user_id): + return result.success(UserManageSerializer.Operate(data={'id': user_id}).one(with_valid=True)) + + @action(methods=['PUT'], detail=False) + @swagger_auto_schema(operation_summary="修改用户信息", + operation_id="修改用户信息", + manual_parameters=UserInstanceSerializer.get_request_params_api(), + request_body=UserManageSerializer.UserEditInstance.get_request_body_api(), + responses=result.get_api_response(UserInstanceSerializer.get_response_body_api()), + tags=["用户管理"] + ) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN], + [PermissionConstants.USER_READ], + compare=CompareConstants.AND)) + def put(self, request: Request, user_id): + return result.success( + UserManageSerializer.Operate(data={'id': user_id}).edit(request.data, with_valid=True))