From 112fabc2bdfbd08bea218467d870b9d66ec3f65d Mon Sep 17 00:00:00 2001 From: nero Date: Thu, 2 Nov 2023 15:24:31 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=85=8D?= =?UTF-8?q?=E7=BD=AE-=20=E8=B4=A6=E6=88=B7=E6=9C=89=E6=95=88=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant/views.py | 8 + .../apis/web/tenant_setting/serializers.py | 29 ++- .../bkuser/apis/web/tenant_setting/urls.py | 5 + .../bkuser/apis/web/tenant_setting/views.py | 77 ++++++- .../0006_tenantuservalidityperiodconfig.py | 33 +++ src/bk-user/bkuser/apps/tenant/models.py | 32 ++- src/bk-user/bkuser/biz/tenant_setting.py | 209 ++++++++++++++++++ 7 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py create mode 100644 src/bk-user/bkuser/biz/tenant_setting.py diff --git a/src/bk-user/bkuser/apis/web/tenant/views.py b/src/bk-user/bkuser/apis/web/tenant/views.py index 5d3635f65..1bb144bad 100644 --- a/src/bk-user/bkuser/apis/web/tenant/views.py +++ b/src/bk-user/bkuser/apis/web/tenant/views.py @@ -34,6 +34,7 @@ TenantHandler, TenantManagerWithoutID, ) +from bkuser.biz.tenant_setting import TenantUserValidityPeriodConfigHandler from bkuser.common.views import ExcludePatchAPIViewMixin from bkuser.plugins.local.models import PasswordInitialConfig @@ -99,8 +100,15 @@ def post(self, request, *args, **kwargs): ] # 本地数据源密码初始化配置 config = PasswordInitialConfig(**data["password_initial_config"]) + + # 创建租户和租户管理员 tenant_id = TenantHandler.create_with_managers(tenant_info, managers, config) + # 创建租户完成后,初始化账号有效期设置 + TenantUserValidityPeriodConfigHandler().init_tenant_user_validity_period_config( + tenant_id, request.user.username + ) + return Response(TenantCreateOutputSLZ(instance={"id": tenant_id}).data) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 59e381d3a..ac236d60f 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -85,7 +85,7 @@ class TenantUserCustomFieldCreateInputSLZ(serializers.Serializer): data_type = serializers.ChoiceField(help_text="字段类型", choices=UserFieldDataType.get_choices()) required = serializers.BooleanField(help_text="是否必填") default = serializers.JSONField(help_text="默认值", required=False) - options = serializers.JSONField(help_text="选项", required=False, default=list) + options = serializers.JSONField(help_text="选项", required=False) def validate_display_name(self, display_name): if TenantUserCustomField.objects.filter( @@ -131,8 +131,8 @@ class TenantUserCustomFieldCreateOutputSLZ(serializers.Serializer): class TenantUserCustomFieldUpdateInputSLZ(serializers.Serializer): display_name = serializers.CharField(help_text="展示用名称", max_length=128) required = serializers.BooleanField(help_text="是否必填") - default = serializers.JSONField(help_text="默认值", required=False) - options = serializers.JSONField(help_text="选项", required=False, default=list) + default = serializers.JSONField(help_text="默认值") + options = serializers.JSONField(help_text="选项") def validate_display_name(self, display_name): if ( @@ -163,3 +163,26 @@ def validate(self, attrs): _validate_multi_enum_default(default, opt_ids) return attrs + + +class NotificationTemplatesInputSLZ(serializers.Serializer): + method = serializers.CharField(help_text="信息传递方式") + scene = serializers.CharField(help_text="通知类型") + title = serializers.CharField(help_text="通知标题", allow_null=True) + sender = serializers.CharField(help_text="发送人") + content = serializers.CharField(help_text="通知内容") + content_html = serializers.CharField(help_text="通知内容,页面展示使用") + + +class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): + enabled_validity_period = serializers.BooleanField(help_text="账户有效期使能值") + valid_time = serializers.IntegerField(help_text="账户有效期") + remind_before_expire = serializers.ListField(help_text="临过期提醒时间") + enabled_notification_methods = serializers.ListField(help_text="通知方式") + notification_templates = serializers.ListField( + help_text="通知模板", child=NotificationTemplatesInputSLZ(), allow_empty=False + ) + + +class TenantUserValidityPeriodConfigOutputSLZ(TenantUserValidityPeriodConfigInputSLZ): + pass diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/urls.py b/src/bk-user/bkuser/apis/web/tenant_setting/urls.py index 50275f541..d585b2bd0 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/urls.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/urls.py @@ -20,4 +20,9 @@ views.TenantUserCustomFieldUpdateDeleteApi.as_view(), name="tenant_setting_custom_fields.update_delete", ), + path( + "settings/tenant-user-validity-period/", + views.TenantUserValidityPeriodConfigRetrieveUpdateApi.as_view(), + name="tenant_user_validity_period_config.retrieve_update", + ), ] diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 35f31d635..46c1c3270 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -8,6 +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 django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status from rest_framework.response import Response @@ -18,9 +19,18 @@ TenantUserCustomFieldCreateOutputSLZ, TenantUserCustomFieldUpdateInputSLZ, TenantUserFieldOutputSLZ, + TenantUserValidityPeriodConfigInputSLZ, + TenantUserValidityPeriodConfigOutputSLZ, ) -from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField -from bkuser.common.views import ExcludePutAPIViewMixin +from bkuser.apps.tenant.models import ( + TenantManager, + TenantUserCustomField, + TenantUserValidityPeriodConfig, + UserBuiltinField, +) +from bkuser.biz.tenant_setting import NotificationTemplate, TenantUserValidityPeriodConfigHandler, ValidityPeriodConfig +from bkuser.common.error_codes import error_codes +from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView): @@ -82,8 +92,7 @@ def put(self, request, *args, **kwargs): tenant_id = self.get_current_tenant_id() slz = TenantUserCustomFieldUpdateInputSLZ( - data=request.data, - context={"tenant_id": tenant_id, "current_custom_field_id": kwargs["id"]}, + data=request.data, context={"tenant_id": tenant_id, "current_custom_field_id": kwargs["id"]} ) slz.is_valid(raise_exception=True) data = slz.validated_data @@ -104,3 +113,63 @@ def put(self, request, *args, **kwargs): ) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + +class TenantUserValidityPeriodConfigRetrieveUpdateApi( + ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView +): + queryset = TenantUserValidityPeriodConfig.objects.all() + + def get_object(self) -> TenantUserValidityPeriodConfig: + tenant_id = self.get_current_tenant_id() + account_validity_period_config = self.queryset.filter(tenant_id=tenant_id).first() + if not account_validity_period_config: + raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) + + return account_validity_period_config + + @swagger_auto_schema( + tags=["tenant-setting"], + operation_description="当前租户的账户有效期配置", + responses={ + status.HTTP_200_OK: TenantUserValidityPeriodConfigOutputSLZ(), + }, + ) + def get(self, request, *args, **kwargs): + instance = self.get_object() + slz = TenantUserValidityPeriodConfigOutputSLZ(instance) + return Response(slz.data) + + @swagger_auto_schema( + tags=["tenant-setting"], + operation_description="更新当前租户的账户有效期配置", + request_body=TenantUserValidityPeriodConfigInputSLZ(), + responses={ + status.HTTP_200_OK: "", + }, + ) + def put(self, request, *args, **kwargs): + tenant_id = self.get_current_tenant_id() + + # 边界限制: 当前租户的管理才可做更新操作 + operator = request.user.username + if not TenantManager.objects.filter(tenant_id=tenant_id, tenant_user_id=operator).exists(): + raise error_codes.NO_PERMISSION + + input_slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data) + input_slz.is_valid(raise_exception=True) + + data = input_slz.validated_data + tenant_user_validity_period_config = ValidityPeriodConfig( + enabled_validity_period=data["enabled_validity_period"], + valid_time=data["valid_time"], + remind_before_expire=data["remind_before_expire"], + enabled_notification_methods=data["enabled_notification_methods"], + notification_templates=[NotificationTemplate(**item) for item in data["notification_templates"]], + ) + + TenantUserValidityPeriodConfigHandler.update_tenant_user_validity_period_config( + tenant_id, operator, tenant_user_validity_period_config + ) + + return Response() diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py b/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py new file mode 100644 index 000000000..081ccfb28 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.20 on 2023-11-01 08:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenant', '0005_builtin_user_fields'), + ] + + operations = [ + migrations.CreateModel( + name='TenantUserValidityPeriodConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('creator', models.CharField(blank=True, max_length=128, null=True)), + ('updater', models.CharField(blank=True, max_length=128, null=True)), + ('enabled_validity_period', models.BooleanField(default=True, verbose_name='是否启用')), + ('valid_time', models.IntegerField(default=-1, verbose_name='有效期(单位:天)')), + ('remind_before_expire', models.JSONField(default=list, verbose_name='临X天过期发送提醒(单位:天)')), + ('enabled_notification_methods', models.JSONField(default=dict, verbose_name='通知方式')), + ('notification_templates', models.JSONField(default=dict, verbose_name='通知模板')), + ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant', unique=True)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index 771fbb4b8..e04972e40 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -10,12 +10,12 @@ """ from django.conf import settings from django.db import models +from django.utils import timezone from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType from bkuser.common.constants import PERMANENT_TIME, BkLanguageEnum -from bkuser.common.models import TimestampedModel -from bkuser.common.time import datetime_to_display +from bkuser.common.models import AuditedModel, TimestampedModel from .constants import TIME_ZONE_CHOICES @@ -81,7 +81,8 @@ class Meta: @property def account_expired_at_display(self) -> str: - return datetime_to_display(self.account_expired_at) + local_time = timezone.localtime(self.account_expired_at) + return local_time.strftime("%Y-%m-%d %H:%M:%S") class TenantDepartment(TimestampedModel): @@ -143,21 +144,16 @@ class Meta: ] -# # TODO: 是否直接定义 TenantCommonConfig 表,AccountValidityPeriod是一个JSON字段? -# class AccountValidityPeriodConfig: -# """账号时效配置""" -# -# tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True, unique=True) -# -# enabled = models.BooleanField("是否启用", default=True) -# # TODO: 定义枚举,设置默认值为永久 -# validity_period_seconds = models.IntegerField("有效期(单位:秒)", default=-1) -# # TODO: 定义枚举,设置默认值为7天 -# reminder_period_days = models.IntegerField("提醒周期(单位:天)", default=7) -# # TODO: 定义枚举,同时需要考虑到与企业ESB配置的支持的通知方式有关,是否定义字段? -# notification_method = models.CharField("通知方式", max_length=32, default="email") -# # TODO: 需要考虑不同通知方式,可能无法使用相同模板,或者其他设计方式 -# notification_content_template = models.TextField("通知模板", default="") +class TenantUserValidityPeriodConfig(AuditedModel): + """账号有效期-配置""" + + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True, unique=True) + + enabled_validity_period = models.BooleanField("是否启用", default=True) + valid_time = models.IntegerField("有效期(单位:天)", default=-1) + remind_before_expire = models.JSONField("临X天过期发送提醒(单位:天)", default=list) + enabled_notification_methods = models.JSONField("通知方式", default=dict) + notification_templates = models.JSONField("通知模板", default=dict) # class TenantUserSocialAccountRelation(TimestampedModel): diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py new file mode 100644 index 000000000..4774951ce --- /dev/null +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +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 blue_krill.data_types.enum import EnumField, StructuredEnum +from django.db import transaction +from django.utils.translation import gettext_lazy as _ +from pydantic import BaseModel, model_validator + +from bkuser.apps.tenant.models import Tenant, TenantUserValidityPeriodConfig +from bkuser.common.error_codes import error_codes + + +class NotificationMethod(str, StructuredEnum): + """通知方式""" + + EMAIL = EnumField("email", label=_("邮件通知")) + SMS = EnumField("sms", label=_("短信通知")) + + +class NotificationScene(str, StructuredEnum): + """通知场景""" + + ACCOUNT_EXPIRING = EnumField("account_expiring", label=_("临过期提醒")) + ACCOUNT_EXPIRED = EnumField("account_expired", label=_("过期提醒")) + + +class NotificationTemplate(BaseModel): + """通知模板""" + + # 通知方式 如短信,邮件 + method: NotificationMethod + # 通知场景 如将过期,已过期 + scene: NotificationScene + # 模板标题 + title: Optional[str] = None + # 模板发送方 + sender: str + # 模板内容(text)格式 + content: str + # 模板内容(html)格式 + content_html: str + + @model_validator(mode="after") + def validate_attrs(self) -> "NotificationTemplate": + if self.method == NotificationMethod.EMAIL and not self.title: + raise ValueError(_("邮件通知模板需要提供标题")) + + return self + + +class ValidityPeriodConfig(BaseModel): + """账号有效期配置""" + + # 使能账号有效期设置 + enabled_validity_period: bool + # 有效期,单位:天 + valid_time: int + # 临X天过期发送提醒, 单位:天 + remind_before_expire: List[int] + # 通知方式 + enabled_notification_methods: List[NotificationMethod] + # 通知模板 + notification_templates: List[NotificationTemplate] + + +class TenantUserValidityPeriodConfigHandler: + # 账户有效期配置初始化,默认值 + DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = { + "enabled_validity_period": True, + "valid_time": -1, + "remind_before_expire": [7], + "enabled_notification_methods": [NotificationMethod.EMAIL], + "notification_templates": [ + { + "method": NotificationMethod.EMAIL, + "scene": NotificationScene.ACCOUNT_EXPIRING, + "title": "蓝鲸智云 - 账号即将到期提醒!", + "sender": "蓝鲸智云", + "content": ( + "{{ username }}, 您好:\n" + + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n" + + "此邮件为系统自动发送,请勿回复。\n" + ), + "content_html": ( + "

{{ username }}, 您好:

" + + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.EMAIL, + "scene": NotificationScene.ACCOUNT_EXPIRED, + "title": "蓝鲸智云 - 账号到期提醒!", + "sender": "蓝鲸智云", + "content": ( + "{{ username }},您好:\n" + + " 您的蓝鲸智云平台账号已过期。如需继续使用,请尽快联系平台管理员进行续期。\n" + + " 此邮件为系统自动发送,请勿回复。" + ), + "content_html": ( + "

{{ username }},您好:

" + + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.SMS, + "scene": NotificationScene.ACCOUNT_EXPIRING, + "title": None, + "sender": "蓝鲸智云", + "content": ( + "{{ username }},您好:\n" + + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n" + + "该短信为系统自动发送,请勿回复。" + ), + "content_html": ( + "

{{ username }},您好:

" + + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。

" + + "

该短信为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.SMS, + "scene": NotificationScene.ACCOUNT_EXPIRED, + "title": None, + "sender": "蓝鲸智云", + "content": ( + "{{ username }},您好:\n " + + "您的蓝鲸智云平台账号已过期。如需继续使用,请尽快联系平台管理员进行续期。\n " + + "该短信为系统自动发送,请勿回复。" + ), + "content_html": ( + "

{{ username }}您好:

" + + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" + + "

该短信为系统自动发送,请勿回复。

" + ), + }, + ], + } + + def init_tenant_user_validity_period_config( + self, + tenant_id: str, + operator: str, + ): + """ + 租户创建完成后, 初始化账户有效期设置 + """ + tenant = Tenant.objects.filter(id=tenant_id).first() + if not tenant: + raise error_codes.OBJECT_NOT_FOUND + + validity_period_config = self.DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG + + TenantUserValidityPeriodConfig.objects.create( + tenant=tenant, + enabled_validity_period=validity_period_config["enabled_validity_period"], + valid_time=validity_period_config["valid_time"], + remind_before_expire=validity_period_config["remind_before_expire"], + enabled_notification_methods=validity_period_config["enabled_notification_methods"], + notification_templates=validity_period_config["notification_templates"], + updater=operator, + creator=operator, + ) + + @staticmethod + def update_tenant_user_validity_period_config( + tenant_id: str, operator: str, validity_period_config: ValidityPeriodConfig + ): + instance = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() + if not instance: + raise error_codes.OBJECT_NOT_FOUND + + notification_templates = [item.model_dump() for item in validity_period_config.notification_templates] + with transaction.atomic(): + instance.enabled_validity_period = validity_period_config.enabled_validity_period + instance.valid_time = validity_period_config.valid_time + instance.remind_before_expire = validity_period_config.remind_before_expire + instance.enabled_notification_methods = validity_period_config.enabled_notification_methods + instance.notification_templates = notification_templates + instance.updater = operator + instance.save() + + @staticmethod + def get_tenant_user_validity_period_config(tenant_id: str) -> ValidityPeriodConfig: + instance = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() + if not instance: + raise error_codes.OBJECT_NOT_FOUND + + return ValidityPeriodConfig( + enabled_validity_period=instance.enabled_validity_period, + valid_time=instance.valid_time, + remind_before_expire=instance.remind_before_expire, + enabled_notification_methods=instance.enabled_notification_methods, + notification_templates=instance.notification_templates, + ) From 2ca445da3969efc8ff32b25d00a98c62c158799e Mon Sep 17 00:00:00 2001 From: nero Date: Thu, 2 Nov 2023 20:21:18 +0800 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E7=94=A8=E6=88=B7=EF=BC=8C=E6=A0=B9=E6=8D=AE=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E8=AE=BE=E7=BD=AE=E8=B4=A6=E6=88=B7=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/sync/syncers.py | 11 +++++++++-- .../bkuser/biz/data_source_organization.py | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/bk-user/bkuser/apps/sync/syncers.py b/src/bk-user/bkuser/apps/sync/syncers.py index 40a1a83bf..8c4078059 100644 --- a/src/bk-user/bkuser/apps/sync/syncers.py +++ b/src/bk-user/bkuser/apps/sync/syncers.py @@ -26,7 +26,7 @@ from bkuser.apps.sync.context import DataSourceSyncTaskContext, TenantSyncTaskContext from bkuser.apps.sync.converters import DataSourceUserConverter from bkuser.apps.sync.models import DataSourceSyncTask, TenantSyncTask -from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantUser +from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantUser, TenantUserValidityPeriodConfig from bkuser.common.constants import PERMANENT_TIME from bkuser.plugins.models import RawDataSourceDepartment, RawDataSourceUser from bkuser.utils.tree import bfs_traversal_tree, build_forest_with_parent_relations @@ -520,6 +520,7 @@ def sync(self): waiting_sync_data_source_users = data_source_users.exclude( id__in=[u.data_source_user_id for u in exists_tenant_users] ) + waiting_create_tenant_users = [ TenantUser( id=generate_uuid(), @@ -538,4 +539,10 @@ def sync(self): def _get_user_account_expired_at(self) -> datetime.datetime: """FIXME (su) 支持读取账号有效期配置,然后累加到 timezone.now() 上,目前是直接返回 PERMANENT_TIME""" - return PERMANENT_TIME + # 根据配置初始化账号有效期 + valid_time = PERMANENT_TIME + validity_period_config = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant.id) + if validity_period_config.enabled_validity_period and validity_period_config.valid_time > 0: + valid_time = timezone.now() + datetime.timedelta(days=validity_period_config.valid_time) + + return valid_time diff --git a/src/bk-user/bkuser/biz/data_source_organization.py b/src/bk-user/bkuser/biz/data_source_organization.py index f7a4691bf..9f9cb5ab9 100644 --- a/src/bk-user/bkuser/biz/data_source_organization.py +++ b/src/bk-user/bkuser/biz/data_source_organization.py @@ -8,10 +8,12 @@ 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. """ +import datetime from collections import defaultdict from typing import Dict, List from django.db import transaction +from django.utils import timezone from pydantic import BaseModel from bkuser.apps.data_source.models import ( @@ -22,6 +24,7 @@ DataSourceUserLeaderRelation, ) from bkuser.apps.tenant.models import Tenant, TenantUser +from bkuser.biz.tenant_setting import TenantUserValidityPeriodConfigHandler from bkuser.utils.uuid import generate_uuid @@ -101,15 +104,27 @@ def create_user( DataSourceUserLeaderRelation.objects.bulk_create(user_leader_relation_objs) # 查询关联的租户 - tenant = Tenant.objects.get(id=data_source.owner_tenant_id) + tenant_id = data_source.owner_tenant_id + tenant = Tenant.objects.get(id=tenant_id) + # 创建租户用户 - TenantUser.objects.create( + validity_period_config = TenantUserValidityPeriodConfigHandler.get_tenant_user_validity_period_config( + tenant_id=tenant_id + ) + tenant_user = TenantUser( data_source_user=user, tenant=tenant, data_source=data_source, id=generate_uuid(), ) + # 根据配置初始化账号有效期 + if validity_period_config.enabled_validity_period and validity_period_config.valid_time > 0: + tenant_user.account_expired_at = timezone.now() + datetime.timedelta( + days=validity_period_config.valid_time + ) + # 入库 + tenant_user.save() return user.id @staticmethod From 4fc127a54b0cf9ed6fdffc0ff04fbc6475344cc3 Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 3 Nov 2023 10:51:06 +0800 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20cr=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/sync/syncers.py | 1 - src/bk-user/bkuser/biz/tenant_setting.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bk-user/bkuser/apps/sync/syncers.py b/src/bk-user/bkuser/apps/sync/syncers.py index 8c4078059..0309d3d86 100644 --- a/src/bk-user/bkuser/apps/sync/syncers.py +++ b/src/bk-user/bkuser/apps/sync/syncers.py @@ -538,7 +538,6 @@ def sync(self): self.ctx.recorder.add(SyncOperation.CREATE, TenantSyncObjectType.USER, waiting_create_tenant_users) def _get_user_account_expired_at(self) -> datetime.datetime: - """FIXME (su) 支持读取账号有效期配置,然后累加到 timezone.now() 上,目前是直接返回 PERMANENT_TIME""" # 根据配置初始化账号有效期 valid_time = PERMANENT_TIME validity_period_config = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant.id) diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index 4774951ce..592f120aa 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -104,9 +104,9 @@ class TenantUserValidityPeriodConfigHandler: "title": "蓝鲸智云 - 账号到期提醒!", "sender": "蓝鲸智云", "content": ( - "{{ username }},您好:\n" - + " 您的蓝鲸智云平台账号已过期。如需继续使用,请尽快联系平台管理员进行续期。\n" - + " 此邮件为系统自动发送,请勿回复。" + "{{ username }},您好:\n" + + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n" + + " 该短信为系统自动发送,请勿回复。" ), "content_html": ( "

{{ username }},您好:

" @@ -138,8 +138,8 @@ class TenantUserValidityPeriodConfigHandler: "title": None, "sender": "蓝鲸智云", "content": ( - "{{ username }},您好:\n " - + "您的蓝鲸智云平台账号已过期。如需继续使用,请尽快联系平台管理员进行续期。\n " + "{{ username }}您好:\n" + + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n" + "该短信为系统自动发送,请勿回复。" ), "content_html": ( From 872ce1a6c2de1039ed1bc96a41880644b262a506 Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 3 Nov 2023 11:19:49 +0800 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20cr=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/biz/tenant_setting.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index 592f120aa..b75d9dc8e 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -104,9 +104,9 @@ class TenantUserValidityPeriodConfigHandler: "title": "蓝鲸智云 - 账号到期提醒!", "sender": "蓝鲸智云", "content": ( - "{{ username }},您好:\n" - + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n" - + " 该短信为系统自动发送,请勿回复。" + "{{ username }},您好:\n" + + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 + + "该短信为系统自动发送,请勿回复。" ), "content_html": ( "

{{ username }},您好:

" @@ -138,9 +138,9 @@ class TenantUserValidityPeriodConfigHandler: "title": None, "sender": "蓝鲸智云", "content": ( - "{{ username }}您好:\n" - + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n" - + "该短信为系统自动发送,请勿回复。" + "{{ username }}您好:\n" # noqa: E501 + + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 + + "该短信为系统自动发送,请勿回复。" # noqa: E501 ), "content_html": ( "

{{ username }}您好:

" From 4b96497b09b23af8368ba3adaa8a052d452b6a7c Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 6 Nov 2023 16:29:19 +0800 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E8=BF=87?= =?UTF-8?q?=E6=9C=9F/=E4=B8=B4=E6=9C=9F=E7=94=A8=E6=88=B7,=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/constants.py | 14 ++ ...007_alter_tenantuser_account_expired_at.py | 19 +++ ...d_tenant_user_expire_nitification_tasks.py | 35 ++++ src/bk-user/bkuser/apps/tenant/models.py | 2 +- src/bk-user/bkuser/apps/tenant/notifier.py | 150 ++++++++++++++++++ .../bkuser/apps/tenant/periodic_tasks.py | 81 ++++++++++ src/bk-user/bkuser/biz/tenant_setting.py | 16 +- 7 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py create mode 100644 src/bk-user/bkuser/apps/tenant/notifier.py create mode 100644 src/bk-user/bkuser/apps/tenant/periodic_tasks.py diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index 5bcd3347f..198039425 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -32,3 +32,17 @@ class UserFieldDataType(str, StructuredEnum): NUMBER = EnumField("number", label=_("数字")) ENUM = EnumField("enum", label=_("枚举")) MULTI_ENUM = EnumField("multi_enum", label=_("多选枚举")) + + +class NotificationMethod(str, StructuredEnum): + """通知方式""" + + EMAIL = EnumField("email", label=_("邮件通知")) + SMS = EnumField("sms", label=_("短信通知")) + + +class NotificationScene(str, StructuredEnum): + """通知场景""" + + TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("临过期提醒")) + TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("过期提醒")) diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py b/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py new file mode 100644 index 000000000..85e1dab03 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.20 on 2023-11-06 04:10 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenant', '0006_tenantuservalidityperiodconfig'), + ] + + operations = [ + migrations.AlterField( + model_name='tenantuser', + name='account_expired_at', + field=models.DateField(blank=True, default=datetime.datetime(2100, 1, 1, 0, 0), null=True, verbose_name='账号过期时间'), + ), + ] diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py b/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py new file mode 100644 index 000000000..ff8a50b90 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.20 on 2023-11-06 04:10 + +import datetime + +import pytz +from django.db import migrations, models +from django_celery_beat.models import CrontabSchedule, PeriodicTask + + +def generate_periodic_tasks(apps, schema_editor): + crontab, _ = CrontabSchedule.objects.update_or_create( + minute="0", + hour="0", + day_of_week="*", + day_of_month='*', + month_of_year='*', + timezone=pytz.timezone("Asia/Shanghai"), + ) + for periodic_task_name in ["send_tenant_user_expiring_notification", "send_tenant_user_expired_notification"]: + PeriodicTask.objects.get_or_create( + name=periodic_task_name, + defaults={ + "crontab": crontab, + "task": f"bkuser.apps.tenant.periodic_tasks.{periodic_task_name}", + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenant', '0007_alter_tenantuser_account_expired_at'), + ] + + operations = [migrations.RunPython(generate_periodic_tasks)] diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index e04972e40..b708a3999 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -59,7 +59,7 @@ class TenantUser(TimestampedModel): wx_openid = models.CharField("微信公众号OpenID", null=True, blank=True, default="", max_length=64) # 账号有效期相关 - account_expired_at = models.DateTimeField("账号过期时间", null=True, blank=True, default=PERMANENT_TIME) + account_expired_at = models.DateField("账号过期时间", null=True, blank=True, default=PERMANENT_TIME) # 手机&邮箱相关:手机号&邮箱都可以继承数据源或自定义 is_inherited_phone = models.BooleanField("是否继承数据源手机号", default=True) diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py new file mode 100644 index 000000000..897bb0a3d --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +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. +""" +import logging +from typing import Dict, List, Optional + +from django.template import Context, Template +from django.utils.translation import gettext_lazy as _ +from pydantic import BaseModel, model_validator + +from bkuser.apps.tenant.constants import NotificationMethod, NotificationScene +from bkuser.apps.tenant.models import TenantUser, TenantUserValidityPeriodConfig +from bkuser.component import cmsi + +logger = logging.getLogger(__name__) + + +class NotificationTemplate(BaseModel): + """通知模板""" + + # 通知方式 如短信,邮件 + method: NotificationMethod + # 通知场景 如将过期,已过期 + scene: NotificationScene + # 模板标题 + title: Optional[str] = None + # 模板发送方 + sender: str + # 模板内容(text)格式 + content: str + # 模板内容(html)格式 + content_html: str + + @model_validator(mode="after") + def validate_attrs(self) -> "NotificationTemplate": + if self.method == NotificationMethod.EMAIL and not self.title: + raise ValueError(_("邮件通知模板需要提供标题")) + + return self + + +class ValidityPeriodNotificationTmplContextGenerator: + """生成通知模板使用的上下文""" + + def __init__(self, user: TenantUser, scene: NotificationScene): + self.user = user + self.scene = scene + + def gen(self) -> Dict[str, str]: + """生成通知模板使用的上下文 + + 注:为保证模板渲染准确性,value 值类型需为 str + """ + if self.scene == NotificationScene.TENANT_USER_EXPIRING: + return self._gen_tenant_user_expiring_ctx() + if self.scene == NotificationScene.TENANT_USER_EXPIRED: + return self._gen_tenant_user_expired_ctx() + return self._gen_base_ctx() + + def _gen_base_ctx(self) -> Dict[str, str]: + """获取基础信息""" + return {"username": self.user.data_source_user.username} + + def _gen_tenant_user_expiring_ctx(self) -> Dict[str, str]: + """账号有效期-临期通知渲染参数""" + # FIXME (su) 提供修改密码的 URL(settings.BK_USER_URL + xxxx) + return { + "expired_at": self.user.account_expired_at_display, + **self._gen_base_ctx(), + } + + def _gen_tenant_user_expired_ctx(self) -> Dict[str, str]: + """账号有效期-过期通知渲染参数""" + return self._gen_base_ctx() + + +class TenantUserValidityPeriodNotifier: + """租户用户用户通知器,支持批量像用户发送某类信息""" + + templates: List[NotificationTemplate] = [] + + def __init__(self, tenant_id: str, scene: NotificationScene): + self.tenant_id = tenant_id + self.scene = scene + + self.templates = self._get_templates_with_scene(scene) + + def send(self, users: List[TenantUser]) -> None: + """根据配置,发送对应的通知信息""" + try: + for u in users: + self._send_notifications(u) + # TODO (su) 细化异常处理 + except Exception: + logger.exception("send notification failed") + + def _get_templates_with_scene(self, scene: NotificationScene) -> List[NotificationTemplate]: + """根据场景以及插件配置中设置的通知方式,获取需要发送通知的模板""" + + if scene not in [NotificationScene.TENANT_USER_EXPIRED, NotificationScene.TENANT_USER_EXPIRING]: + raise ValueError(_("通知场景 {} 未被支持".format(scene))) + + # 获取通知配置 + validity_period_config = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant_id) + templates = validity_period_config.notification_templates + enabled_methods = validity_period_config.enabled_notification_methods + + # 返回场景匹配,且被声明启用的模板列表 + return [ + NotificationTemplate(**tmpl) + for tmpl in templates + if validity_period_config["scene"] == scene and tmpl["method"] in enabled_methods + ] + + def _send_notifications(self, user: TenantUser): + """根据配置的通知模板,逐个用户发送通知""" + for tmpl in self.templates: + if tmpl.method == NotificationMethod.EMAIL: + self._send_email(user, tmpl) + elif tmpl.method == NotificationMethod.SMS: + self._send_sms(user, tmpl) + + def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): + logger.info( + "send email to user %s, scene %s, title: %s", user.data_source_user.username, tmpl.scene, tmpl.title + ) + content = self._render_tmpl(user, tmpl.content_html) + # FIXME (su) 修改为指定用户名 + # 根据继承与否,获取真实邮箱 + email = user.data_source_user.email if user.is_inherited_email else user.custom_email + cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore + + def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate): + logger.info("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) + content = self._render_tmpl(user, tmpl.content) + # FIXME (su) 修改为指定用户名 + # 根据继承与否,获取真实手机号 + phone = user.data_source_user.phone if user.is_inherited_phone else user.custom_phone + cmsi.send_sms([phone], content) + + def _render_tmpl(self, user: TenantUser, content: str) -> str: + ctx = ValidityPeriodNotificationTmplContextGenerator(user=user, scene=self.scene).gen() + return Template(content).render(Context(ctx)) diff --git a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py new file mode 100644 index 000000000..8defaa0f7 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +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. +""" +import datetime +import logging + +from django.utils import timezone + +from bkuser.apps.tenant.constants import NotificationScene +from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig +from bkuser.apps.tenant.notifier import TenantUserValidityPeriodNotifier +from bkuser.celery import app +from bkuser.common.task import BaseTask + +logger = logging.getLogger(__name__) + + +@app.task(base=BaseTask, ignore_result=True) +def send_tenant_user_expiring_notification(): + """扫描全部租户用户,做临期通知""" + logger.info("[celery] receive period task:send_tenant_user_expiring_notification") + now = timezone.now() + + # 获取 租户-未过期用户 映射 + tenant_ids = Tenant.objects.filter().values_list("id", flat=True) + tenant_users_map = { + tenant_id: TenantUser.objects.filter(account_expired_at__gt=now.date(), tenant_id=tenant_id) + for tenant_id in tenant_ids + } + + # 获取账号有效期-临期配置 + tenant_remind_before_expire_map = { + tenant_id: TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first().remind_before_expire + for tenant_id in tenant_ids + } + + for tenant_id, remind_before_expire in tenant_remind_before_expire_map.items(): + tenant_users = tenant_users_map[tenant_id] + + # 临1/7/15天过期 条件设置, 每个租户的设置都不一样 + account_expired_at_conditions = [ + now.date() + datetime.timedelta(days=int(remain_days)) for remain_days in remind_before_expire + ] + should_notify_users = list(tenant_users.filter(account_expired_at__in=account_expired_at_conditions)) + # 发送通知 + logger.info("going to notify expiring users in tenant-%s, count: %s", tenant_id, len(should_notify_users)) + + TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRING).send( + should_notify_users + ) + + +@app.task(base=BaseTask, ignore_result=True) +def send_tenant_user_expired_notification(): + """扫描全部租户用户,做过期通知""" + logger.info("[celery] receive period task:send_tenant_user_expired_notification") + + # 今日过期 + now_date = timezone.now().date() + + # 获取 租户-过期用户 + tenant_ids = Tenant.objects.filter().values_list("id", flat=True) + tenant_user_map = { + tenant_id: TenantUser.objects.filter(account_expired_at=now_date, tenant_id=tenant_id) + for tenant_id in tenant_ids + } + + for tenant_id, tenant_users in tenant_user_map.items(): + # 发送通知 过期 + logger.info("going to notify expired users in tenant-%s, count: %s", tenant_id, len(tenant_users)) + + TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRED).send( + list(tenant_users) + ) diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index b75d9dc8e..abbd5907f 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -29,8 +29,8 @@ class NotificationMethod(str, StructuredEnum): class NotificationScene(str, StructuredEnum): """通知场景""" - ACCOUNT_EXPIRING = EnumField("account_expiring", label=_("临过期提醒")) - ACCOUNT_EXPIRED = EnumField("account_expired", label=_("过期提醒")) + TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("临过期提醒")) + TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("过期提醒")) class NotificationTemplate(BaseModel): @@ -82,7 +82,7 @@ class TenantUserValidityPeriodConfigHandler: "notification_templates": [ { "method": NotificationMethod.EMAIL, - "scene": NotificationScene.ACCOUNT_EXPIRING, + "scene": NotificationScene.TENANT_USER_EXPIRING, "title": "蓝鲸智云 - 账号即将到期提醒!", "sender": "蓝鲸智云", "content": ( @@ -100,13 +100,13 @@ class TenantUserValidityPeriodConfigHandler: }, { "method": NotificationMethod.EMAIL, - "scene": NotificationScene.ACCOUNT_EXPIRED, + "scene": NotificationScene.TENANT_USER_EXPIRED, "title": "蓝鲸智云 - 账号到期提醒!", "sender": "蓝鲸智云", "content": ( "{{ username }},您好:\n" + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 - + "该短信为系统自动发送,请勿回复。" + + "该邮件为系统自动发送,请勿回复。" ), "content_html": ( "

{{ username }},您好:

" @@ -116,7 +116,7 @@ class TenantUserValidityPeriodConfigHandler: }, { "method": NotificationMethod.SMS, - "scene": NotificationScene.ACCOUNT_EXPIRING, + "scene": NotificationScene.TENANT_USER_EXPIRING, "title": None, "sender": "蓝鲸智云", "content": ( @@ -134,11 +134,11 @@ class TenantUserValidityPeriodConfigHandler: }, { "method": NotificationMethod.SMS, - "scene": NotificationScene.ACCOUNT_EXPIRED, + "scene": NotificationScene.TENANT_USER_EXPIRED, "title": None, "sender": "蓝鲸智云", "content": ( - "{{ username }}您好:\n" # noqa: E501 + "{{ username }}您好:\n" + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 + "该短信为系统自动发送,请勿回复。" # noqa: E501 ), From c0c7602f465520073c13872773883cdfed809438 Mon Sep 17 00:00:00 2001 From: nero Date: Tue, 7 Nov 2023 15:58:13 +0800 Subject: [PATCH 06/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bkuser/apis/web/tenant_setting/views.py | 18 ++- src/bk-user/bkuser/biz/tenant_setting.py | 121 ++++++++++-------- 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 46c1c3270..d7044e731 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -122,11 +122,11 @@ class TenantUserValidityPeriodConfigRetrieveUpdateApi( def get_object(self) -> TenantUserValidityPeriodConfig: tenant_id = self.get_current_tenant_id() - account_validity_period_config = self.queryset.filter(tenant_id=tenant_id).first() - if not account_validity_period_config: + tenant_user_validity_period_config = self.queryset.filter(tenant_id=tenant_id).first() + if not tenant_user_validity_period_config: raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) - return account_validity_period_config + return tenant_user_validity_period_config @swagger_auto_schema( tags=["tenant-setting"], @@ -165,7 +165,17 @@ def put(self, request, *args, **kwargs): valid_time=data["valid_time"], remind_before_expire=data["remind_before_expire"], enabled_notification_methods=data["enabled_notification_methods"], - notification_templates=[NotificationTemplate(**item) for item in data["notification_templates"]], + notification_templates=[ + NotificationTemplate( + method=template["method"], + scene=template["scene"], + title=template.get("title", None), + sender=template["sender"], + content=template["content"], + content_html=template["content_html"], + ) + for template in data["notification_templates"] + ], ) TenantUserValidityPeriodConfigHandler.update_tenant_user_validity_period_config( diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index abbd5907f..d94663e8a 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -74,82 +74,82 @@ class ValidityPeriodConfig(BaseModel): class TenantUserValidityPeriodConfigHandler: # 账户有效期配置初始化,默认值 - DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = { - "enabled_validity_period": True, - "valid_time": -1, - "remind_before_expire": [7], - "enabled_notification_methods": [NotificationMethod.EMAIL], - "notification_templates": [ - { - "method": NotificationMethod.EMAIL, - "scene": NotificationScene.TENANT_USER_EXPIRING, - "title": "蓝鲸智云 - 账号即将到期提醒!", - "sender": "蓝鲸智云", - "content": ( - "{{ username }}, 您好:\n" + DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = ValidityPeriodConfig( + enabled_validity_period=True, + valid_time=-1, + remind_before_expire=[7], + enabled_notification_methods=[NotificationMethod.EMAIL], + notification_templates=[ + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.TENANT_USER_EXPIRING, + title="蓝鲸智云 - 账号即将到期提醒!", + sender="蓝鲸智云", + content=( + "{{ username }}, 您好:\n " + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。\n" - + "此邮件为系统自动发送,请勿回复。\n" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + + "此邮件为系统自动发送,请勿回复。\n " ), - "content_html": ( + content_html=( "

{{ username }}, 您好:

" + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。

" + "

此邮件为系统自动发送,请勿回复。

" ), - }, - { - "method": NotificationMethod.EMAIL, - "scene": NotificationScene.TENANT_USER_EXPIRED, - "title": "蓝鲸智云 - 账号到期提醒!", - "sender": "蓝鲸智云", - "content": ( - "{{ username }},您好:\n" - + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 + ), + NotificationTemplate( + method=NotificationMethod.EMAIL, + scene=NotificationScene.TENANT_USER_EXPIRED, + title="蓝鲸智云 - 账号到期提醒!", + sender="蓝鲸智云", + content=( + "{{ username }},您好:\n " + + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 + "该邮件为系统自动发送,请勿回复。" ), - "content_html": ( + content_html=( "

{{ username }},您好:

" + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" + "

此邮件为系统自动发送,请勿回复。

" ), - }, - { - "method": NotificationMethod.SMS, - "scene": NotificationScene.TENANT_USER_EXPIRING, - "title": None, - "sender": "蓝鲸智云", - "content": ( - "{{ username }},您好:\n" + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.TENANT_USER_EXPIRING, + title=None, + sender="蓝鲸智云", + content=( + "{{ username }},您好:\n " + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。\n" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + "该短信为系统自动发送,请勿回复。" ), - "content_html": ( + content_html=( "

{{ username }},您好:

" + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。

" + "

该短信为系统自动发送,请勿回复。

" ), - }, - { - "method": NotificationMethod.SMS, - "scene": NotificationScene.TENANT_USER_EXPIRED, - "title": None, - "sender": "蓝鲸智云", - "content": ( - "{{ username }}您好:\n" - + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n" # noqa: E501 + ), + NotificationTemplate( + method=NotificationMethod.SMS, + scene=NotificationScene.TENANT_USER_EXPIRED, + title=None, + sender="蓝鲸智云", + content=( + "{{ username }}您好:\n " + + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 + "该短信为系统自动发送,请勿回复。" # noqa: E501 ), - "content_html": ( + content_html=( "

{{ username }}您好:

" + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" + "

该短信为系统自动发送,请勿回复。

" ), - }, + ), ], - } + ) def init_tenant_user_validity_period_config( self, @@ -165,13 +165,14 @@ def init_tenant_user_validity_period_config( validity_period_config = self.DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG + notification_templates = [template.model_dump() for template in validity_period_config.notification_templates] TenantUserValidityPeriodConfig.objects.create( tenant=tenant, - enabled_validity_period=validity_period_config["enabled_validity_period"], - valid_time=validity_period_config["valid_time"], - remind_before_expire=validity_period_config["remind_before_expire"], - enabled_notification_methods=validity_period_config["enabled_notification_methods"], - notification_templates=validity_period_config["notification_templates"], + enabled_validity_period=validity_period_config.enabled_validity_period, + valid_time=validity_period_config.valid_time, + remind_before_expire=validity_period_config.remind_before_expire, + enabled_notification_methods=validity_period_config.enabled_notification_methods, + notification_templates=notification_templates, updater=operator, creator=operator, ) @@ -184,7 +185,7 @@ def update_tenant_user_validity_period_config( if not instance: raise error_codes.OBJECT_NOT_FOUND - notification_templates = [item.model_dump() for item in validity_period_config.notification_templates] + notification_templates = [template.model_dump() for template in validity_period_config.notification_templates] with transaction.atomic(): instance.enabled_validity_period = validity_period_config.enabled_validity_period instance.valid_time = validity_period_config.valid_time @@ -205,5 +206,15 @@ def get_tenant_user_validity_period_config(tenant_id: str) -> ValidityPeriodConf valid_time=instance.valid_time, remind_before_expire=instance.remind_before_expire, enabled_notification_methods=instance.enabled_notification_methods, - notification_templates=instance.notification_templates, + notification_templates=[ + NotificationTemplate( + method=template["method"], + scene=template["scene"], + title=template.get("title", None), + sender=template["sender"], + content=template["content"], + content_html=template["content_html"], + ) + for template in instance.notification_templates + ], ) From abb1e07f3f2108202b9e3f1c6c9cf827d0bca798 Mon Sep 17 00:00:00 2001 From: nero Date: Tue, 7 Nov 2023 16:45:02 +0800 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index b708a3999..6102f38a5 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -10,7 +10,6 @@ """ from django.conf import settings from django.db import models -from django.utils import timezone from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType @@ -81,8 +80,7 @@ class Meta: @property def account_expired_at_display(self) -> str: - local_time = timezone.localtime(self.account_expired_at) - return local_time.strftime("%Y-%m-%d %H:%M:%S") + return self.account_expired_at.strftime("%Y-%m-%d") class TenantDepartment(TimestampedModel): From c4010796f112c03a5e0d3dde21e70d7203808f8e Mon Sep 17 00:00:00 2001 From: nero Date: Tue, 7 Nov 2023 19:08:22 +0800 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/constants.py | 4 ++-- src/bk-user/bkuser/biz/tenant_setting.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index 198039425..926f0d7c5 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -44,5 +44,5 @@ class NotificationMethod(str, StructuredEnum): class NotificationScene(str, StructuredEnum): """通知场景""" - TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("临过期提醒")) - TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("过期提醒")) + TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户-临过期提醒")) + TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户-过期提醒")) diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index d94663e8a..e93df3b4b 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -29,8 +29,8 @@ class NotificationMethod(str, StructuredEnum): class NotificationScene(str, StructuredEnum): """通知场景""" - TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("临过期提醒")) - TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("过期提醒")) + TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户-临过期提醒")) + TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户-过期提醒")) class NotificationTemplate(BaseModel): From 65de1890ab03b09df8eedeaf62b09758ee19b905 Mon Sep 17 00:00:00 2001 From: nero Date: Tue, 7 Nov 2023 20:26:36 +0800 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/web/tenant_setting/serializers.py | 11 +++--- .../bkuser/apis/web/tenant_setting/views.py | 21 +++++------ src/bk-user/bkuser/apps/sync/syncers.py | 11 ++---- .../0006_tenantuservalidityperiodconfig.py | 33 ----------------- ...007_alter_tenantuser_account_expired_at.py | 19 ---------- ...d_tenant_user_expire_nitification_tasks.py | 35 ------------------- src/bk-user/bkuser/apps/tenant/models.py | 8 ++--- src/bk-user/bkuser/apps/tenant/notifier.py | 3 -- .../bkuser/apps/tenant/periodic_tasks.py | 11 ++++-- src/bk-user/bkuser/settings.py | 12 ++++++- 10 files changed, 42 insertions(+), 122 deletions(-) delete mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py delete mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py delete mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index ac236d60f..1bb2bdb6e 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -19,6 +19,7 @@ from bkuser.apps.tenant.constants import UserFieldDataType from bkuser.apps.tenant.data_models import TenantUserCustomFieldOptions from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField +from bkuser.biz.tenant_setting import NotificationMethod logger = logging.getLogger(__name__) @@ -85,7 +86,7 @@ class TenantUserCustomFieldCreateInputSLZ(serializers.Serializer): data_type = serializers.ChoiceField(help_text="字段类型", choices=UserFieldDataType.get_choices()) required = serializers.BooleanField(help_text="是否必填") default = serializers.JSONField(help_text="默认值", required=False) - options = serializers.JSONField(help_text="选项", required=False) + options = serializers.JSONField(help_text="选项", required=False, default=list) def validate_display_name(self, display_name): if TenantUserCustomField.objects.filter( @@ -131,8 +132,8 @@ class TenantUserCustomFieldCreateOutputSLZ(serializers.Serializer): class TenantUserCustomFieldUpdateInputSLZ(serializers.Serializer): display_name = serializers.CharField(help_text="展示用名称", max_length=128) required = serializers.BooleanField(help_text="是否必填") - default = serializers.JSONField(help_text="默认值") - options = serializers.JSONField(help_text="选项") + default = serializers.JSONField(help_text="默认值", required=False) + options = serializers.JSONField(help_text="选项", required=False, default=list) def validate_display_name(self, display_name): if ( @@ -166,7 +167,7 @@ def validate(self, attrs): class NotificationTemplatesInputSLZ(serializers.Serializer): - method = serializers.CharField(help_text="信息传递方式") + method = serializers.ChoiceField(help_text="信息传递方式", choices=NotificationMethod.get_choices()) scene = serializers.CharField(help_text="通知类型") title = serializers.CharField(help_text="通知标题", allow_null=True) sender = serializers.CharField(help_text="发送人") @@ -175,7 +176,7 @@ class NotificationTemplatesInputSLZ(serializers.Serializer): class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): - enabled_validity_period = serializers.BooleanField(help_text="账户有效期使能值") + enabled_validity_period = serializers.BooleanField(help_text="是否启用账户有效期") valid_time = serializers.IntegerField(help_text="账户有效期") remind_before_expire = serializers.ListField(help_text="临过期提醒时间") enabled_notification_methods = serializers.ListField(help_text="通知方式") diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index d7044e731..fa25faebe 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -118,15 +118,8 @@ def delete(self, request, *args, **kwargs): class TenantUserValidityPeriodConfigRetrieveUpdateApi( ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView ): - queryset = TenantUserValidityPeriodConfig.objects.all() - - def get_object(self) -> TenantUserValidityPeriodConfig: - tenant_id = self.get_current_tenant_id() - tenant_user_validity_period_config = self.queryset.filter(tenant_id=tenant_id).first() - if not tenant_user_validity_period_config: - raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) - - return tenant_user_validity_period_config + def get_queryset(self): + return TenantUserValidityPeriodConfig.objects.filter(tenant_id=self.get_current_tenant_id()) @swagger_auto_schema( tags=["tenant-setting"], @@ -136,7 +129,11 @@ def get_object(self) -> TenantUserValidityPeriodConfig: }, ) def get(self, request, *args, **kwargs): - instance = self.get_object() + instance = self.get_queryset().first() + + if not instance: + raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) + slz = TenantUserValidityPeriodConfigOutputSLZ(instance) return Response(slz.data) @@ -145,7 +142,7 @@ def get(self, request, *args, **kwargs): operation_description="更新当前租户的账户有效期配置", request_body=TenantUserValidityPeriodConfigInputSLZ(), responses={ - status.HTTP_200_OK: "", + status.HTTP_204_NO_CONTENT: "", }, ) def put(self, request, *args, **kwargs): @@ -182,4 +179,4 @@ def put(self, request, *args, **kwargs): tenant_id, operator, tenant_user_validity_period_config ) - return Response() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apps/sync/syncers.py b/src/bk-user/bkuser/apps/sync/syncers.py index 264dd6e72..73982c963 100644 --- a/src/bk-user/bkuser/apps/sync/syncers.py +++ b/src/bk-user/bkuser/apps/sync/syncers.py @@ -25,7 +25,7 @@ from bkuser.apps.sync.constants import DataSourceSyncObjectType, SyncOperation, TenantSyncObjectType from bkuser.apps.sync.context import DataSourceSyncTaskContext, TenantSyncTaskContext from bkuser.apps.sync.converters import DataSourceUserConverter -from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantUser, TenantUserValidityPeriodConfig +from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantUser from bkuser.common.constants import PERMANENT_TIME from bkuser.plugins.models import RawDataSourceDepartment, RawDataSourceUser from bkuser.utils.tree import bfs_traversal_tree, build_forest_with_parent_relations @@ -533,10 +533,5 @@ def sync(self): self.ctx.recorder.add(SyncOperation.CREATE, TenantSyncObjectType.USER, waiting_create_tenant_users) def _get_user_account_expired_at(self) -> datetime.datetime: - # 根据配置初始化账号有效期 - valid_time = PERMANENT_TIME - validity_period_config = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant.id) - if validity_period_config.enabled_validity_period and validity_period_config.valid_time > 0: - valid_time = timezone.now() + datetime.timedelta(days=validity_period_config.valid_time) - - return valid_time + """FIXME (su) 支持读取账号有效期配置,然后累加到 timezone.now() 上,目前是直接返回 PERMANENT_TIME""" + return PERMANENT_TIME diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py b/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py deleted file mode 100644 index 081ccfb28..000000000 --- a/src/bk-user/bkuser/apps/tenant/migrations/0006_tenantuservalidityperiodconfig.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.20 on 2023-11-01 08:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenant', '0005_builtin_user_fields'), - ] - - operations = [ - migrations.CreateModel( - name='TenantUserValidityPeriodConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('creator', models.CharField(blank=True, max_length=128, null=True)), - ('updater', models.CharField(blank=True, max_length=128, null=True)), - ('enabled_validity_period', models.BooleanField(default=True, verbose_name='是否启用')), - ('valid_time', models.IntegerField(default=-1, verbose_name='有效期(单位:天)')), - ('remind_before_expire', models.JSONField(default=list, verbose_name='临X天过期发送提醒(单位:天)')), - ('enabled_notification_methods', models.JSONField(default=dict, verbose_name='通知方式')), - ('notification_templates', models.JSONField(default=dict, verbose_name='通知模板')), - ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant', unique=True)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py b/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py deleted file mode 100644 index 85e1dab03..000000000 --- a/src/bk-user/bkuser/apps/tenant/migrations/0007_alter_tenantuser_account_expired_at.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.20 on 2023-11-06 04:10 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenant', '0006_tenantuservalidityperiodconfig'), - ] - - operations = [ - migrations.AlterField( - model_name='tenantuser', - name='account_expired_at', - field=models.DateField(blank=True, default=datetime.datetime(2100, 1, 1, 0, 0), null=True, verbose_name='账号过期时间'), - ), - ] diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py b/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py deleted file mode 100644 index ff8a50b90..000000000 --- a/src/bk-user/bkuser/apps/tenant/migrations/0008_add_tenant_user_expire_nitification_tasks.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.2.20 on 2023-11-06 04:10 - -import datetime - -import pytz -from django.db import migrations, models -from django_celery_beat.models import CrontabSchedule, PeriodicTask - - -def generate_periodic_tasks(apps, schema_editor): - crontab, _ = CrontabSchedule.objects.update_or_create( - minute="0", - hour="0", - day_of_week="*", - day_of_month='*', - month_of_year='*', - timezone=pytz.timezone("Asia/Shanghai"), - ) - for periodic_task_name in ["send_tenant_user_expiring_notification", "send_tenant_user_expired_notification"]: - PeriodicTask.objects.get_or_create( - name=periodic_task_name, - defaults={ - "crontab": crontab, - "task": f"bkuser.apps.tenant.periodic_tasks.{periodic_task_name}", - }, - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenant', '0007_alter_tenantuser_account_expired_at'), - ] - - operations = [migrations.RunPython(generate_periodic_tasks)] diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index 6102f38a5..11ef9a8f0 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -145,13 +145,13 @@ class Meta: class TenantUserValidityPeriodConfig(AuditedModel): """账号有效期-配置""" - tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True, unique=True) + tenant = models.OneToOneField(Tenant, on_delete=models.CASCADE, db_index=True, unique=True) - enabled_validity_period = models.BooleanField("是否启用", default=True) + enabled_validity_period = models.BooleanField("是否启用账户有效期", default=True) valid_time = models.IntegerField("有效期(单位:天)", default=-1) remind_before_expire = models.JSONField("临X天过期发送提醒(单位:天)", default=list) - enabled_notification_methods = models.JSONField("通知方式", default=dict) - notification_templates = models.JSONField("通知模板", default=dict) + enabled_notification_methods = models.JSONField("通知方式", default=list) + notification_templates = models.JSONField("通知模板", default=list) # class TenantUserSocialAccountRelation(TimestampedModel): diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 897bb0a3d..dffbe5d56 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -70,7 +70,6 @@ def _gen_base_ctx(self) -> Dict[str, str]: def _gen_tenant_user_expiring_ctx(self) -> Dict[str, str]: """账号有效期-临期通知渲染参数""" - # FIXME (su) 提供修改密码的 URL(settings.BK_USER_URL + xxxx) return { "expired_at": self.user.account_expired_at_display, **self._gen_base_ctx(), @@ -132,7 +131,6 @@ def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): "send email to user %s, scene %s, title: %s", user.data_source_user.username, tmpl.scene, tmpl.title ) content = self._render_tmpl(user, tmpl.content_html) - # FIXME (su) 修改为指定用户名 # 根据继承与否,获取真实邮箱 email = user.data_source_user.email if user.is_inherited_email else user.custom_email cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore @@ -140,7 +138,6 @@ def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate): logger.info("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) content = self._render_tmpl(user, tmpl.content) - # FIXME (su) 修改为指定用户名 # 根据继承与否,获取真实手机号 phone = user.data_source_user.phone if user.is_inherited_phone else user.custom_phone cmsi.send_sms([phone], content) diff --git a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py index 8defaa0f7..e33e39837 100644 --- a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py +++ b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py @@ -29,7 +29,7 @@ def send_tenant_user_expiring_notification(): now = timezone.now() # 获取 租户-未过期用户 映射 - tenant_ids = Tenant.objects.filter().values_list("id", flat=True) + tenant_ids = Tenant.objects.all().values_list("id", flat=True) tenant_users_map = { tenant_id: TenantUser.objects.filter(account_expired_at__gt=now.date(), tenant_id=tenant_id) for tenant_id in tenant_ids @@ -52,6 +52,9 @@ def send_tenant_user_expiring_notification(): # 发送通知 logger.info("going to notify expiring users in tenant-%s, count: %s", tenant_id, len(should_notify_users)) + if not should_notify_users: + return + TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRING).send( should_notify_users ) @@ -76,6 +79,10 @@ def send_tenant_user_expired_notification(): # 发送通知 过期 logger.info("going to notify expired users in tenant-%s, count: %s", tenant_id, len(tenant_users)) + should_notify_users = list(tenant_users) + if not should_notify_users: + return + TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRED).send( - list(tenant_users) + should_notify_users ) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index a2c9e549f..2d1328b41 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -16,6 +16,7 @@ import environ import urllib3 +from celery.schedules import crontab from django.utils.encoding import force_bytes # environ @@ -241,7 +242,16 @@ # CELERY 配置,申明任务的文件路径,即包含有 @task 装饰器的函数文件 # CELERY_IMPORTS = [] # 内置的周期任务 -# CELERYBEAT_SCHEDULE = {} +CELERYBEAT_SCHEDULE = { + "periodic_notify_expiring_tenant_users": { + "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expiring_notification", + "schedule": crontab(minute="0", hour="0"), # 每天凌晨0时执行 + }, + "periodic_notify_expired_tenant_users": { + "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expired_notification", + "schedule": crontab(minute="0", hour="4"), # 每天凌晨4时执行 + }, +} # Celery 消息队列配置 CELERY_BROKER_URL = env.str("BK_BROKER_URL", default="") From 6791a948b4c7b3ee915f8864176c4abb088c42e5 Mon Sep 17 00:00:00 2001 From: nero Date: Wed, 8 Nov 2023 19:06:42 +0800 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant/views.py | 6 - .../apis/web/tenant_setting/serializers.py | 4 +- .../bkuser/apis/web/tenant_setting/views.py | 54 +++--- src/bk-user/bkuser/apps/tenant/constants.py | 78 ++++++++ src/bk-user/bkuser/apps/tenant/models.py | 8 +- src/bk-user/bkuser/apps/tenant/notifier.py | 6 +- .../bkuser/apps/tenant/periodic_tasks.py | 38 ++-- .../bkuser/biz/data_source_organization.py | 16 +- src/bk-user/bkuser/biz/tenant.py | 12 +- src/bk-user/bkuser/biz/tenant_setting.py | 169 +----------------- 10 files changed, 160 insertions(+), 231 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant/views.py b/src/bk-user/bkuser/apis/web/tenant/views.py index 1bb144bad..c11c5335d 100644 --- a/src/bk-user/bkuser/apis/web/tenant/views.py +++ b/src/bk-user/bkuser/apis/web/tenant/views.py @@ -34,7 +34,6 @@ TenantHandler, TenantManagerWithoutID, ) -from bkuser.biz.tenant_setting import TenantUserValidityPeriodConfigHandler from bkuser.common.views import ExcludePatchAPIViewMixin from bkuser.plugins.local.models import PasswordInitialConfig @@ -104,11 +103,6 @@ def post(self, request, *args, **kwargs): # 创建租户和租户管理员 tenant_id = TenantHandler.create_with_managers(tenant_info, managers, config) - # 创建租户完成后,初始化账号有效期设置 - TenantUserValidityPeriodConfigHandler().init_tenant_user_validity_period_config( - tenant_id, request.user.username - ) - return Response(TenantCreateOutputSLZ(instance={"id": tenant_id}).data) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 1bb2bdb6e..753439be1 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -176,8 +176,8 @@ class NotificationTemplatesInputSLZ(serializers.Serializer): class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): - enabled_validity_period = serializers.BooleanField(help_text="是否启用账户有效期") - valid_time = serializers.IntegerField(help_text="账户有效期") + enabled = serializers.BooleanField(help_text="是否启用账户有效期") + validity_period = serializers.IntegerField(help_text="账户有效期") remind_before_expire = serializers.ListField(help_text="临过期提醒时间") enabled_notification_methods = serializers.ListField(help_text="通知方式") notification_templates = serializers.ListField( diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index fa25faebe..877fdf810 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -28,7 +28,7 @@ TenantUserValidityPeriodConfig, UserBuiltinField, ) -from bkuser.biz.tenant_setting import NotificationTemplate, TenantUserValidityPeriodConfigHandler, ValidityPeriodConfig +from bkuser.biz.tenant_setting import NotificationTemplate from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin @@ -146,37 +146,37 @@ def get(self, request, *args, **kwargs): }, ) def put(self, request, *args, **kwargs): - tenant_id = self.get_current_tenant_id() + instance = self.get_queryset().first() # 边界限制: 当前租户的管理才可做更新操作 operator = request.user.username - if not TenantManager.objects.filter(tenant_id=tenant_id, tenant_user_id=operator).exists(): + if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists(): raise error_codes.NO_PERMISSION - input_slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data) - input_slz.is_valid(raise_exception=True) - - data = input_slz.validated_data - tenant_user_validity_period_config = ValidityPeriodConfig( - enabled_validity_period=data["enabled_validity_period"], - valid_time=data["valid_time"], - remind_before_expire=data["remind_before_expire"], - enabled_notification_methods=data["enabled_notification_methods"], - notification_templates=[ - NotificationTemplate( - method=template["method"], - scene=template["scene"], - title=template.get("title", None), - sender=template["sender"], - content=template["content"], - content_html=template["content_html"], - ) - for template in data["notification_templates"] - ], - ) + slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data) + slz.is_valid(raise_exception=True) - TenantUserValidityPeriodConfigHandler.update_tenant_user_validity_period_config( - tenant_id, operator, tenant_user_validity_period_config - ) + data = slz.validated_data + # 校验模板 + [ + NotificationTemplate( + method=template["method"], + scene=template["scene"], + title=template.get("title"), + sender=template["sender"], + content=template["content"], + content_html=template["content_html"], + ) + for template in data["notification_templates"] + ] + + instance.enabled = data["enabled"] + instance.valid_time = data["validity_period"] + instance.remind_before_expire = data["remind_before_expire"] + instance.enabled_notification_methods = data["enabled_notification_methods"] + instance.notification_templates = data["notification_templates"] + instance.updater = operator + + instance.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index 926f0d7c5..13a090d98 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -46,3 +46,81 @@ class NotificationScene(str, StructuredEnum): TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户-临过期提醒")) TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户-过期提醒")) + + +DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = { + "enabled": True, + "validity_period": -1, + "remind_before_expire": [7], + "enabled_notification_methods": [NotificationMethod.EMAIL], + "notification_templates": [ + { + "method": NotificationMethod.EMAIL, + "scene": NotificationScene.TENANT_USER_EXPIRING, + "title": "蓝鲸智云 - 账号即将到期提醒!", + "sender": "蓝鲸智云", + "content": ( + "{{ username }}, 您好:\n " + + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + + "此邮件为系统自动发送,请勿回复。\n " + ), + "content_html": ( + "

{{ username }}, 您好:

" + + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。

" + + "

此邮件为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.EMAIL, + "scene": NotificationScene.TENANT_USER_EXPIRED, + "title": "蓝鲸智云 - 账号到期提醒!", + "sender": "蓝鲸智云", + "content": ( + "{{ username }},您好:\n " + + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 + + "该邮件为系统自动发送,请勿回复。" # noqa: E501 + ), + "content_html": ( + "

{{ username }},您好:

" + + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" # noqa: E501 + + "

此邮件为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.SMS, + "scene": NotificationScene.TENANT_USER_EXPIRING, + "title": None, + "sender": "蓝鲸智云", + "content": ( + "{{ username }},您好:\n " + + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + + "该短信为系统自动发送,请勿回复。" + ), + "content_html": ( + "

{{ username }},您好:

" + + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "为避免影响使用,请尽快联系平台管理员进行续期。

" + + "

该短信为系统自动发送,请勿回复。

" + ), + }, + { + "method": NotificationMethod.SMS, + "scene": NotificationScene.TENANT_USER_EXPIRED, + "title": None, + "sender": "蓝鲸智云", + "content": ( + "{{ username }}您好:\n " + + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 + + "该短信为系统自动发送,请勿回复。" # noqa: E501 + ), + "content_html": ( + "

{{ username }}您好:

" + + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" # noqa: E501 + + "

该短信为系统自动发送,请勿回复。

" + ), + }, + ], +} diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index 11ef9a8f0..c6b3843c9 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -55,10 +55,10 @@ class TenantUser(TimestampedModel): # wx_userid/wx_openid 兼容旧版本迁移 wx_userid = models.CharField("微信ID", null=True, blank=True, default="", max_length=64) - wx_openid = models.CharField("微信公众号OpenID", null=True, blank=True, default="", max_length=64) + wx_openid = models.CharField("微信公众号 用户OpenID", null=True, blank=True, default="", max_length=64) # 账号有效期相关 - account_expired_at = models.DateField("账号过期时间", null=True, blank=True, default=PERMANENT_TIME) + account_expired_at = models.DateTimeField("账号过期时间", null=True, blank=True, default=PERMANENT_TIME) # 手机&邮箱相关:手机号&邮箱都可以继承数据源或自定义 is_inherited_phone = models.BooleanField("是否继承数据源手机号", default=True) @@ -147,8 +147,8 @@ class TenantUserValidityPeriodConfig(AuditedModel): tenant = models.OneToOneField(Tenant, on_delete=models.CASCADE, db_index=True, unique=True) - enabled_validity_period = models.BooleanField("是否启用账户有效期", default=True) - valid_time = models.IntegerField("有效期(单位:天)", default=-1) + enabled = models.BooleanField("是否启用账户有效期", default=True) + validity_period = models.IntegerField("有效期(单位:天)", default=-1) remind_before_expire = models.JSONField("临X天过期发送提醒(单位:天)", default=list) enabled_notification_methods = models.JSONField("通知方式", default=list) notification_templates = models.JSONField("通知模板", default=list) diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index dffbe5d56..6837e2b6e 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -115,7 +115,7 @@ def _get_templates_with_scene(self, scene: NotificationScene) -> List[Notificati return [ NotificationTemplate(**tmpl) for tmpl in templates - if validity_period_config["scene"] == scene and tmpl["method"] in enabled_methods + if tmpl["scene"] == scene and tmpl["method"] in enabled_methods ] def _send_notifications(self, user: TenantUser): @@ -127,7 +127,7 @@ def _send_notifications(self, user: TenantUser): self._send_sms(user, tmpl) def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): - logger.info( + logger.debug( "send email to user %s, scene %s, title: %s", user.data_source_user.username, tmpl.scene, tmpl.title ) content = self._render_tmpl(user, tmpl.content_html) @@ -136,7 +136,7 @@ def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate): - logger.info("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) + logger.debug("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) content = self._render_tmpl(user, tmpl.content) # 根据继承与否,获取真实手机号 phone = user.data_source_user.phone if user.is_inherited_phone else user.custom_phone diff --git a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py index e33e39837..a4285f20d 100644 --- a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py +++ b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py @@ -27,11 +27,12 @@ def send_tenant_user_expiring_notification(): """扫描全部租户用户,做临期通知""" logger.info("[celery] receive period task:send_tenant_user_expiring_notification") now = timezone.now() - # 获取 租户-未过期用户 映射 tenant_ids = Tenant.objects.all().values_list("id", flat=True) tenant_users_map = { - tenant_id: TenantUser.objects.filter(account_expired_at__gt=now.date(), tenant_id=tenant_id) + tenant_id: TenantUser.objects.filter( + account_expired_at__gt=now.replace(hour=0, minute=0, second=0), tenant_id=tenant_id + ) for tenant_id in tenant_ids } @@ -45,12 +46,19 @@ def send_tenant_user_expiring_notification(): tenant_users = tenant_users_map[tenant_id] # 临1/7/15天过期 条件设置, 每个租户的设置都不一样 - account_expired_at_conditions = [ - now.date() + datetime.timedelta(days=int(remain_days)) for remain_days in remind_before_expire - ] - should_notify_users = list(tenant_users.filter(account_expired_at__in=account_expired_at_conditions)) + should_notify_users = [] + for remain_days in remind_before_expire: + account_expired_date = now + datetime.timedelta(days=int(remain_days)) + should_notify_users += list( + tenant_users.filter( + account_expired_at__range=[ + account_expired_date.replace(hour=0, minute=0, second=0), + account_expired_date.replace(hour=23, minute=59, second=59, microsecond=999999), + ] + ) + ) # 发送通知 - logger.info("going to notify expiring users in tenant-%s, count: %s", tenant_id, len(should_notify_users)) + logger.info("going to notify expiring users in tenant{%s}, count: %s", tenant_id, len(should_notify_users)) if not should_notify_users: return @@ -66,20 +74,24 @@ def send_tenant_user_expired_notification(): logger.info("[celery] receive period task:send_tenant_user_expired_notification") # 今日过期 - now_date = timezone.now().date() - + now = timezone.now() # 获取 租户-过期用户 tenant_ids = Tenant.objects.filter().values_list("id", flat=True) tenant_user_map = { - tenant_id: TenantUser.objects.filter(account_expired_at=now_date, tenant_id=tenant_id) + tenant_id: TenantUser.objects.filter( + account_expired_at__range=[ + now.replace(hour=0, minute=0, second=0), + now.replace(hour=23, minute=59, second=59, microsecond=999999), + ], + tenant_id=tenant_id, + ) for tenant_id in tenant_ids } for tenant_id, tenant_users in tenant_user_map.items(): - # 发送通知 过期 - logger.info("going to notify expired users in tenant-%s, count: %s", tenant_id, len(tenant_users)) - + # 发送过期通知 should_notify_users = list(tenant_users) + logger.info("going to notify expired users in tenant{%s}, count: %s", tenant_id, len(should_notify_users)) if not should_notify_users: return diff --git a/src/bk-user/bkuser/biz/data_source_organization.py b/src/bk-user/bkuser/biz/data_source_organization.py index 9f9cb5ab9..17b026df7 100644 --- a/src/bk-user/bkuser/biz/data_source_organization.py +++ b/src/bk-user/bkuser/biz/data_source_organization.py @@ -14,6 +14,7 @@ from django.db import transaction from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from pydantic import BaseModel from bkuser.apps.data_source.models import ( @@ -23,8 +24,8 @@ DataSourceUser, DataSourceUserLeaderRelation, ) -from bkuser.apps.tenant.models import Tenant, TenantUser -from bkuser.biz.tenant_setting import TenantUserValidityPeriodConfigHandler +from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig +from bkuser.common.error_codes import error_codes from bkuser.utils.uuid import generate_uuid @@ -108,9 +109,10 @@ def create_user( tenant = Tenant.objects.get(id=tenant_id) # 创建租户用户 - validity_period_config = TenantUserValidityPeriodConfigHandler.get_tenant_user_validity_period_config( - tenant_id=tenant_id - ) + validity_period_config = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() + if not validity_period_config: + raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) + tenant_user = TenantUser( data_source_user=user, tenant=tenant, @@ -119,9 +121,9 @@ def create_user( ) # 根据配置初始化账号有效期 - if validity_period_config.enabled_validity_period and validity_period_config.valid_time > 0: + if validity_period_config.enabled and validity_period_config.validity_period > 0: tenant_user.account_expired_at = timezone.now() + datetime.timedelta( - days=validity_period_config.valid_time + days=validity_period_config.validity_period ) # 入库 tenant_user.save() diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py index 83da08107..af50a85dc 100644 --- a/src/bk-user/bkuser/biz/tenant.py +++ b/src/bk-user/bkuser/biz/tenant.py @@ -18,7 +18,14 @@ from pydantic import BaseModel from bkuser.apps.data_source.models import DataSourceDepartmentRelation, DataSourceUser -from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantManager, TenantUser +from bkuser.apps.tenant.constants import DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG +from bkuser.apps.tenant.models import ( + Tenant, + TenantDepartment, + TenantManager, + TenantUser, + TenantUserValidityPeriodConfig, +) from bkuser.biz.data_source import ( DataSourceDepartmentHandler, DataSourceHandler, @@ -290,6 +297,9 @@ def create_with_managers( # 创建租户本身 tenant = Tenant.objects.create(**tenant_info.model_dump()) + # 创建租户完成后,初始化账号有效期设置 + TenantUserValidityPeriodConfig.objects.create(tenant=tenant, **DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG) + # 创建本地数据源 data_source = DataSourceHandler.create_local_data_source_with_merge_config( _("{}-本地数据源").format(tenant_info.name), tenant.id, password_initial_config diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py index e93df3b4b..06511376e 100644 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ b/src/bk-user/bkuser/biz/tenant_setting.py @@ -8,16 +8,12 @@ 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 Optional from blue_krill.data_types.enum import EnumField, StructuredEnum -from django.db import transaction from django.utils.translation import gettext_lazy as _ from pydantic import BaseModel, model_validator -from bkuser.apps.tenant.models import Tenant, TenantUserValidityPeriodConfig -from bkuser.common.error_codes import error_codes - class NotificationMethod(str, StructuredEnum): """通知方式""" @@ -55,166 +51,3 @@ def validate_attrs(self) -> "NotificationTemplate": raise ValueError(_("邮件通知模板需要提供标题")) return self - - -class ValidityPeriodConfig(BaseModel): - """账号有效期配置""" - - # 使能账号有效期设置 - enabled_validity_period: bool - # 有效期,单位:天 - valid_time: int - # 临X天过期发送提醒, 单位:天 - remind_before_expire: List[int] - # 通知方式 - enabled_notification_methods: List[NotificationMethod] - # 通知模板 - notification_templates: List[NotificationTemplate] - - -class TenantUserValidityPeriodConfigHandler: - # 账户有效期配置初始化,默认值 - DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = ValidityPeriodConfig( - enabled_validity_period=True, - valid_time=-1, - remind_before_expire=[7], - enabled_notification_methods=[NotificationMethod.EMAIL], - notification_templates=[ - NotificationTemplate( - method=NotificationMethod.EMAIL, - scene=NotificationScene.TENANT_USER_EXPIRING, - title="蓝鲸智云 - 账号即将到期提醒!", - sender="蓝鲸智云", - content=( - "{{ username }}, 您好:\n " - + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。\n " - + "此邮件为系统自动发送,请勿回复。\n " - ), - content_html=( - "

{{ username }}, 您好:

" - + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。

" - + "

此邮件为系统自动发送,请勿回复。

" - ), - ), - NotificationTemplate( - method=NotificationMethod.EMAIL, - scene=NotificationScene.TENANT_USER_EXPIRED, - title="蓝鲸智云 - 账号到期提醒!", - sender="蓝鲸智云", - content=( - "{{ username }},您好:\n " - + "您的蓝鲸智云平台账号已过期。为避免影响使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 - + "该邮件为系统自动发送,请勿回复。" - ), - content_html=( - "

{{ username }},您好:

" - + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" - + "

此邮件为系统自动发送,请勿回复。

" - ), - ), - NotificationTemplate( - method=NotificationMethod.SMS, - scene=NotificationScene.TENANT_USER_EXPIRING, - title=None, - sender="蓝鲸智云", - content=( - "{{ username }},您好:\n " - + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。\n " - + "该短信为系统自动发送,请勿回复。" - ), - content_html=( - "

{{ username }},您好:

" - + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" - + "为避免影响使用,请尽快联系平台管理员进行续期。

" - + "

该短信为系统自动发送,请勿回复。

" - ), - ), - NotificationTemplate( - method=NotificationMethod.SMS, - scene=NotificationScene.TENANT_USER_EXPIRED, - title=None, - sender="蓝鲸智云", - content=( - "{{ username }}您好:\n " - + "您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。\n " # noqa: E501 - + "该短信为系统自动发送,请勿回复。" # noqa: E501 - ), - content_html=( - "

{{ username }}您好:

" - + "

您的蓝鲸智云平台账号已过期,如需继续使用,请尽快联系平台管理员进行续期。

" - + "

该短信为系统自动发送,请勿回复。

" - ), - ), - ], - ) - - def init_tenant_user_validity_period_config( - self, - tenant_id: str, - operator: str, - ): - """ - 租户创建完成后, 初始化账户有效期设置 - """ - tenant = Tenant.objects.filter(id=tenant_id).first() - if not tenant: - raise error_codes.OBJECT_NOT_FOUND - - validity_period_config = self.DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG - - notification_templates = [template.model_dump() for template in validity_period_config.notification_templates] - TenantUserValidityPeriodConfig.objects.create( - tenant=tenant, - enabled_validity_period=validity_period_config.enabled_validity_period, - valid_time=validity_period_config.valid_time, - remind_before_expire=validity_period_config.remind_before_expire, - enabled_notification_methods=validity_period_config.enabled_notification_methods, - notification_templates=notification_templates, - updater=operator, - creator=operator, - ) - - @staticmethod - def update_tenant_user_validity_period_config( - tenant_id: str, operator: str, validity_period_config: ValidityPeriodConfig - ): - instance = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() - if not instance: - raise error_codes.OBJECT_NOT_FOUND - - notification_templates = [template.model_dump() for template in validity_period_config.notification_templates] - with transaction.atomic(): - instance.enabled_validity_period = validity_period_config.enabled_validity_period - instance.valid_time = validity_period_config.valid_time - instance.remind_before_expire = validity_period_config.remind_before_expire - instance.enabled_notification_methods = validity_period_config.enabled_notification_methods - instance.notification_templates = notification_templates - instance.updater = operator - instance.save() - - @staticmethod - def get_tenant_user_validity_period_config(tenant_id: str) -> ValidityPeriodConfig: - instance = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() - if not instance: - raise error_codes.OBJECT_NOT_FOUND - - return ValidityPeriodConfig( - enabled_validity_period=instance.enabled_validity_period, - valid_time=instance.valid_time, - remind_before_expire=instance.remind_before_expire, - enabled_notification_methods=instance.enabled_notification_methods, - notification_templates=[ - NotificationTemplate( - method=template["method"], - scene=template["scene"], - title=template.get("title", None), - sender=template["sender"], - content=template["content"], - content_html=template["content_html"], - ) - for template in instance.notification_templates - ], - ) From b7a317bf64d82912d5fe0ad7d5875eaf278d9575 Mon Sep 17 00:00:00 2001 From: nero Date: Wed, 8 Nov 2023 19:15:36 +0800 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant_setting/views.py | 2 +- src/bk-user/bkuser/apps/tenant/models.py | 7 +++---- src/bk-user/bkuser/apps/tenant/notifier.py | 10 ++++------ src/bk-user/bkuser/biz/data_source_organization.py | 10 ++++------ 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 877fdf810..6a9ebeb67 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -171,7 +171,7 @@ def put(self, request, *args, **kwargs): ] instance.enabled = data["enabled"] - instance.valid_time = data["validity_period"] + instance.validity_period = data["validity_period"] instance.remind_before_expire = data["remind_before_expire"] instance.enabled_notification_methods = data["enabled_notification_methods"] instance.notification_templates = data["notification_templates"] diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index c6b3843c9..36bdf868e 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -12,11 +12,10 @@ from django.db import models from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser -from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType +from bkuser.apps.tenant.constants import TIME_ZONE_CHOICES, TenantFeatureFlag, UserFieldDataType from bkuser.common.constants import PERMANENT_TIME, BkLanguageEnum from bkuser.common.models import AuditedModel, TimestampedModel - -from .constants import TIME_ZONE_CHOICES +from bkuser.common.time import datetime_to_display class Tenant(TimestampedModel): @@ -80,7 +79,7 @@ class Meta: @property def account_expired_at_display(self) -> str: - return self.account_expired_at.strftime("%Y-%m-%d") + return datetime_to_display(self.account_expired_at) class TenantDepartment(TimestampedModel): diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 6837e2b6e..f3cf702d8 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -96,7 +96,7 @@ def send(self, users: List[TenantUser]) -> None: try: for u in users: self._send_notifications(u) - # TODO (su) 细化异常处理 + # TODO 细化异常处理 except Exception: logger.exception("send notification failed") @@ -107,15 +107,13 @@ def _get_templates_with_scene(self, scene: NotificationScene) -> List[Notificati raise ValueError(_("通知场景 {} 未被支持".format(scene))) # 获取通知配置 - validity_period_config = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant_id) - templates = validity_period_config.notification_templates - enabled_methods = validity_period_config.enabled_notification_methods + cfg = TenantUserValidityPeriodConfig.objects.get(tenant_id=self.tenant_id) # 返回场景匹配,且被声明启用的模板列表 return [ NotificationTemplate(**tmpl) - for tmpl in templates - if tmpl["scene"] == scene and tmpl["method"] in enabled_methods + for tmpl in cfg.notification_templates + if tmpl["scene"] == scene and tmpl["method"] in cfg.enabled_notification_methods ] def _send_notifications(self, user: TenantUser): diff --git a/src/bk-user/bkuser/biz/data_source_organization.py b/src/bk-user/bkuser/biz/data_source_organization.py index 17b026df7..80e41242b 100644 --- a/src/bk-user/bkuser/biz/data_source_organization.py +++ b/src/bk-user/bkuser/biz/data_source_organization.py @@ -109,8 +109,8 @@ def create_user( tenant = Tenant.objects.get(id=tenant_id) # 创建租户用户 - validity_period_config = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() - if not validity_period_config: + cfg = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() + if not cfg: raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) tenant_user = TenantUser( @@ -121,10 +121,8 @@ def create_user( ) # 根据配置初始化账号有效期 - if validity_period_config.enabled and validity_period_config.validity_period > 0: - tenant_user.account_expired_at = timezone.now() + datetime.timedelta( - days=validity_period_config.validity_period - ) + if cfg.enabled and cfg.validity_period > 0: + tenant_user.account_expired_at = timezone.now() + datetime.timedelta(days=cfg.validity_period) # 入库 tenant_user.save() return user.id From 7c31c370dfbbd63d51866ad21a796a656c99bc44 Mon Sep 17 00:00:00 2001 From: nero Date: Thu, 9 Nov 2023 15:39:38 +0800 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant_setting/serializers.py | 5 ++--- src/bk-user/bkuser/apps/tenant/constants.py | 8 ++++---- src/bk-user/bkuser/apps/tenant/notifier.py | 8 +++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 753439be1..d06214b2b 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -16,10 +16,9 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from bkuser.apps.tenant.constants import UserFieldDataType +from bkuser.apps.tenant.constants import NotificationMethod, NotificationScene, UserFieldDataType from bkuser.apps.tenant.data_models import TenantUserCustomFieldOptions from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField -from bkuser.biz.tenant_setting import NotificationMethod logger = logging.getLogger(__name__) @@ -168,7 +167,7 @@ def validate(self, attrs): class NotificationTemplatesInputSLZ(serializers.Serializer): method = serializers.ChoiceField(help_text="信息传递方式", choices=NotificationMethod.get_choices()) - scene = serializers.CharField(help_text="通知类型") + scene = serializers.ChoiceField(help_text="通知类型", choices=NotificationScene.get_choices()) title = serializers.CharField(help_text="通知标题", allow_null=True) sender = serializers.CharField(help_text="发送人") content = serializers.CharField(help_text="通知内容") diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index 13a090d98..3d35fd394 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -61,13 +61,13 @@ class NotificationScene(str, StructuredEnum): "sender": "蓝鲸智云", "content": ( "{{ username }}, 您好:\n " - + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + "此邮件为系统自动发送,请勿回复。\n " ), "content_html": ( "

{{ username }}, 您好:

" - + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "

您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。

" + "

此邮件为系统自动发送,请勿回复。

" ), @@ -95,13 +95,13 @@ class NotificationScene(str, StructuredEnum): "sender": "蓝鲸智云", "content": ( "{{ username }},您好:\n " - + "您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。\n " + "该短信为系统自动发送,请勿回复。" ), "content_html": ( "

{{ username }},您好:

" - + "

您的蓝鲸智云平台账号将于 {{ expired_at }} 天后到期。" + + "

您的蓝鲸智云平台账号将于 {{ remind_before_expire_days }} 天后到期。" + "为避免影响使用,请尽快联系平台管理员进行续期。

" + "

该短信为系统自动发送,请勿回复。

" ), diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index f3cf702d8..55cf48790 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -12,6 +12,7 @@ from typing import Dict, List, Optional from django.template import Context, Template +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from pydantic import BaseModel, model_validator @@ -70,8 +71,9 @@ def _gen_base_ctx(self) -> Dict[str, str]: def _gen_tenant_user_expiring_ctx(self) -> Dict[str, str]: """账号有效期-临期通知渲染参数""" + remind_before_expire_day = self.user.account_expired_at - timezone.now() return { - "expired_at": self.user.account_expired_at_display, + "remind_before_expire_days": str(remind_before_expire_day.days), **self._gen_base_ctx(), } @@ -125,7 +127,7 @@ def _send_notifications(self, user: TenantUser): self._send_sms(user, tmpl) def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): - logger.debug( + logger.info( "send email to user %s, scene %s, title: %s", user.data_source_user.username, tmpl.scene, tmpl.title ) content = self._render_tmpl(user, tmpl.content_html) @@ -134,7 +136,7 @@ def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate): - logger.debug("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) + logger.info("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) content = self._render_tmpl(user, tmpl.content) # 根据继承与否,获取真实手机号 phone = user.data_source_user.phone if user.is_inherited_phone else user.custom_phone From 0a2aac799c94acbaafa44746da321315768eb35f Mon Sep 17 00:00:00 2001 From: nero Date: Thu, 9 Nov 2023 16:12:31 +0800 Subject: [PATCH 13/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/notifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 55cf48790..e6146020d 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -73,7 +73,7 @@ def _gen_tenant_user_expiring_ctx(self) -> Dict[str, str]: """账号有效期-临期通知渲染参数""" remind_before_expire_day = self.user.account_expired_at - timezone.now() return { - "remind_before_expire_days": str(remind_before_expire_day.days), + "remind_before_expire_days": str(remind_before_expire_day.days + 1), **self._gen_base_ctx(), } From 383f28631b2dc4be2716f063ac1a032ab23bd827 Mon Sep 17 00:00:00 2001 From: nero Date: Thu, 9 Nov 2023 16:57:50 +0800 Subject: [PATCH 14/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant_setting/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 6a9ebeb67..d225bc3e3 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -28,10 +28,13 @@ TenantUserValidityPeriodConfig, UserBuiltinField, ) +from bkuser.apps.tenant.periodic_tasks import send_tenant_user_expiring_notification from bkuser.biz.tenant_setting import NotificationTemplate from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin +send_tenant_user_expiring_notification() + class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView): pagination_class = None @@ -148,6 +151,9 @@ def get(self, request, *args, **kwargs): def put(self, request, *args, **kwargs): instance = self.get_queryset().first() + if not instance: + raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) + # 边界限制: 当前租户的管理才可做更新操作 operator = request.user.username if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists(): From 6a6db2c273df592843df940229c59eafa91eff58 Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 10 Nov 2023 10:45:59 +0800 Subject: [PATCH 15/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 2d1328b41..1ca642fac 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -245,11 +245,11 @@ CELERYBEAT_SCHEDULE = { "periodic_notify_expiring_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expiring_notification", - "schedule": crontab(minute="0", hour="0"), # 每天凌晨0时执行 + "schedule": crontab(minute="0", hour="10"), # 每天凌晨0时执行 }, "periodic_notify_expired_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expired_notification", - "schedule": crontab(minute="0", hour="4"), # 每天凌晨4时执行 + "schedule": crontab(minute="0", hour="10"), # 每天凌晨4时执行 }, } # Celery 消息队列配置 From e525b6b35466f3d64a0a725db8d795931b7de814 Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 10 Nov 2023 10:47:35 +0800 Subject: [PATCH 16/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 1ca642fac..3381ba0a2 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -245,11 +245,11 @@ CELERYBEAT_SCHEDULE = { "periodic_notify_expiring_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expiring_notification", - "schedule": crontab(minute="0", hour="10"), # 每天凌晨0时执行 + "schedule": crontab(minute="0", hour="10"), # 每天凌晨10时执行 }, "periodic_notify_expired_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expired_notification", - "schedule": crontab(minute="0", hour="10"), # 每天凌晨4时执行 + "schedule": crontab(minute="0", hour="10"), # 每天凌晨10时执行 }, } # Celery 消息队列配置 From 777957edff14303650c383b9522a3fed290c8ec0 Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 10 Nov 2023 10:48:13 +0800 Subject: [PATCH 17/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 3381ba0a2..ea6be8eef 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -245,11 +245,11 @@ CELERYBEAT_SCHEDULE = { "periodic_notify_expiring_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expiring_notification", - "schedule": crontab(minute="0", hour="10"), # 每天凌晨10时执行 + "schedule": crontab(minute="0", hour="10"), # 每天10时执行 }, "periodic_notify_expired_tenant_users": { "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expired_notification", - "schedule": crontab(minute="0", hour="10"), # 每天凌晨10时执行 + "schedule": crontab(minute="0", hour="10"), # 每天10时执行 }, } # Celery 消息队列配置 From 7b954d108040a29bf67f3e5feb2636bc803979df Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 10 Nov 2023 14:59:33 +0800 Subject: [PATCH 18/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/web/tenant_setting/serializers.py | 4 +- .../bkuser/apis/web/tenant_setting/views.py | 16 +----- src/bk-user/bkuser/apps/sync/syncers.py | 1 - src/bk-user/bkuser/apps/tenant/constants.py | 2 +- src/bk-user/bkuser/biz/tenant_setting.py | 53 ------------------- 5 files changed, 4 insertions(+), 72 deletions(-) delete mode 100644 src/bk-user/bkuser/biz/tenant_setting.py diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index d06214b2b..4cc575877 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -166,8 +166,8 @@ def validate(self, attrs): class NotificationTemplatesInputSLZ(serializers.Serializer): - method = serializers.ChoiceField(help_text="信息传递方式", choices=NotificationMethod.get_choices()) - scene = serializers.ChoiceField(help_text="通知类型", choices=NotificationScene.get_choices()) + method = serializers.ChoiceField(help_text="通知方式", choices=NotificationMethod.get_choices()) + scene = serializers.ChoiceField(help_text="通知场景", choices=NotificationScene.get_choices()) title = serializers.CharField(help_text="通知标题", allow_null=True) sender = serializers.CharField(help_text="发送人") content = serializers.CharField(help_text="通知内容") diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index d225bc3e3..28627f3f7 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -29,7 +29,6 @@ UserBuiltinField, ) from bkuser.apps.tenant.periodic_tasks import send_tenant_user_expiring_notification -from bkuser.biz.tenant_setting import NotificationTemplate from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin @@ -154,27 +153,14 @@ def put(self, request, *args, **kwargs): if not instance: raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) - # 边界限制: 当前租户的管理才可做更新操作 + # TODO (su) 权限调整为 perm_class 当前租户的管理才可做更新操作 operator = request.user.username if not TenantManager.objects.filter(tenant_id=instance.tenant_id, tenant_user_id=operator).exists(): raise error_codes.NO_PERMISSION slz = TenantUserValidityPeriodConfigInputSLZ(data=request.data) slz.is_valid(raise_exception=True) - data = slz.validated_data - # 校验模板 - [ - NotificationTemplate( - method=template["method"], - scene=template["scene"], - title=template.get("title"), - sender=template["sender"], - content=template["content"], - content_html=template["content_html"], - ) - for template in data["notification_templates"] - ] instance.enabled = data["enabled"] instance.validity_period = data["validity_period"] diff --git a/src/bk-user/bkuser/apps/sync/syncers.py b/src/bk-user/bkuser/apps/sync/syncers.py index 73982c963..053fed94a 100644 --- a/src/bk-user/bkuser/apps/sync/syncers.py +++ b/src/bk-user/bkuser/apps/sync/syncers.py @@ -515,7 +515,6 @@ def sync(self): waiting_sync_data_source_users = data_source_users.exclude( id__in=[u.data_source_user_id for u in exists_tenant_users] ) - waiting_create_tenant_users = [ TenantUser( id=generate_uuid(), diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index 3d35fd394..a81376df8 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -50,7 +50,7 @@ class NotificationScene(str, StructuredEnum): DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = { "enabled": True, - "validity_period": -1, + "validity_period": 365, "remind_before_expire": [7], "enabled_notification_methods": [NotificationMethod.EMAIL], "notification_templates": [ diff --git a/src/bk-user/bkuser/biz/tenant_setting.py b/src/bk-user/bkuser/biz/tenant_setting.py deleted file mode 100644 index 06511376e..000000000 --- a/src/bk-user/bkuser/biz/tenant_setting.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -""" -TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. -Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. -Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at http://opensource.org/licenses/MIT -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -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 Optional - -from blue_krill.data_types.enum import EnumField, StructuredEnum -from django.utils.translation import gettext_lazy as _ -from pydantic import BaseModel, model_validator - - -class NotificationMethod(str, StructuredEnum): - """通知方式""" - - EMAIL = EnumField("email", label=_("邮件通知")) - SMS = EnumField("sms", label=_("短信通知")) - - -class NotificationScene(str, StructuredEnum): - """通知场景""" - - TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户-临过期提醒")) - TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户-过期提醒")) - - -class NotificationTemplate(BaseModel): - """通知模板""" - - # 通知方式 如短信,邮件 - method: NotificationMethod - # 通知场景 如将过期,已过期 - scene: NotificationScene - # 模板标题 - title: Optional[str] = None - # 模板发送方 - sender: str - # 模板内容(text)格式 - content: str - # 模板内容(html)格式 - content_html: str - - @model_validator(mode="after") - def validate_attrs(self) -> "NotificationTemplate": - if self.method == NotificationMethod.EMAIL and not self.title: - raise ValueError(_("邮件通知模板需要提供标题")) - - return self From 13f3739e17c37f58b8197db8acf0659d08b25fcb Mon Sep 17 00:00:00 2001 From: nero Date: Fri, 10 Nov 2023 15:03:22 +0800 Subject: [PATCH 19/26] refactor: add migrations file --- .../migrations/0006_auto_20231108_1207.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py b/src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py new file mode 100644 index 000000000..73d7ac5e7 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.20 on 2023-11-08 04:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenant', '0005_builtin_user_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='tenantuser', + name='wx_openid', + field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='微信公众号 用户OpenID'), + ), + migrations.CreateModel( + name='TenantUserValidityPeriodConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('creator', models.CharField(blank=True, max_length=128, null=True)), + ('updater', models.CharField(blank=True, max_length=128, null=True)), + ('enabled', models.BooleanField(default=True, verbose_name='是否启用账户有效期')), + ('validity_period', models.IntegerField(default=-1, verbose_name='有效期(单位:天)')), + ('remind_before_expire', models.JSONField(default=list, verbose_name='临X天过期发送提醒(单位:天)')), + ('enabled_notification_methods', models.JSONField(default=list, verbose_name='通知方式')), + ('notification_templates', models.JSONField(default=list, verbose_name='通知模板')), + ('tenant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant')), + ], + options={ + 'abstract': False, + }, + ), + ] From c30cfe98a92fdf06fd5fdcf37639d174b4a8e290 Mon Sep 17 00:00:00 2001 From: nero Date: Sat, 11 Nov 2023 19:01:41 +0800 Subject: [PATCH 20/26] =?UTF-8?q?refactor:=20cr=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/web/tenant_setting/serializers.py | 8 +- .../bkuser/apis/web/tenant_setting/views.py | 27 ++--- src/bk-user/bkuser/apps/tenant/constants.py | 4 +- src/bk-user/bkuser/apps/tenant/models.py | 8 ++ src/bk-user/bkuser/apps/tenant/notifier.py | 28 +++-- .../bkuser/apps/tenant/periodic_tasks.py | 100 ----------------- src/bk-user/bkuser/apps/tenant/tasks.py | 105 ++++++++++++++++++ .../bkuser/biz/data_source_organization.py | 10 +- src/bk-user/bkuser/settings.py | 4 +- 9 files changed, 155 insertions(+), 139 deletions(-) delete mode 100644 src/bk-user/bkuser/apps/tenant/periodic_tasks.py create mode 100644 src/bk-user/bkuser/apps/tenant/tasks.py diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 4cc575877..974b40775 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -176,9 +176,13 @@ class NotificationTemplatesInputSLZ(serializers.Serializer): class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): enabled = serializers.BooleanField(help_text="是否启用账户有效期") - validity_period = serializers.IntegerField(help_text="账户有效期") + validity_period = serializers.IntegerField(help_text="账户有效期,单位:天") remind_before_expire = serializers.ListField(help_text="临过期提醒时间") - enabled_notification_methods = serializers.ListField(help_text="通知方式") + enabled_notification_methods = serializers.ListField( + help_text="通知方式", + child=serializers.ChoiceField(choices=NotificationMethod.get_choices()), + allow_empty=False, + ) notification_templates = serializers.ListField( help_text="通知模板", child=NotificationTemplatesInputSLZ(), allow_empty=False ) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index 28627f3f7..b88567af3 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -8,9 +8,9 @@ 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 django.utils.translation import gettext_lazy as _ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status +from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from bkuser.apis.web.mixins import CurrentUserTenantMixin @@ -28,12 +28,9 @@ TenantUserValidityPeriodConfig, UserBuiltinField, ) -from bkuser.apps.tenant.periodic_tasks import send_tenant_user_expiring_notification from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin, ExcludePutAPIViewMixin -send_tenant_user_expiring_notification() - class TenantUserFieldListApi(CurrentUserTenantMixin, generics.ListAPIView): pagination_class = None @@ -120,8 +117,12 @@ def delete(self, request, *args, **kwargs): class TenantUserValidityPeriodConfigRetrieveUpdateApi( ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView ): - def get_queryset(self): - return TenantUserValidityPeriodConfig.objects.filter(tenant_id=self.get_current_tenant_id()) + queryset = TenantUserValidityPeriodConfig.objects.all() + + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + filter_kwargs = {"tenant_id": self.get_current_tenant_id()} + return get_object_or_404(queryset, **filter_kwargs) @swagger_auto_schema( tags=["tenant-setting"], @@ -131,13 +132,8 @@ def get_queryset(self): }, ) def get(self, request, *args, **kwargs): - instance = self.get_queryset().first() - - if not instance: - raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) - - slz = TenantUserValidityPeriodConfigOutputSLZ(instance) - return Response(slz.data) + instance = self.get_object() + return Response(TenantUserValidityPeriodConfigOutputSLZ(instance).data) @swagger_auto_schema( tags=["tenant-setting"], @@ -148,10 +144,7 @@ def get(self, request, *args, **kwargs): }, ) def put(self, request, *args, **kwargs): - instance = self.get_queryset().first() - - if not instance: - raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) + instance = self.get_object() # TODO (su) 权限调整为 perm_class 当前租户的管理才可做更新操作 operator = request.user.username diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py index a81376df8..8cb4276bf 100644 --- a/src/bk-user/bkuser/apps/tenant/constants.py +++ b/src/bk-user/bkuser/apps/tenant/constants.py @@ -44,8 +44,8 @@ class NotificationMethod(str, StructuredEnum): class NotificationScene(str, StructuredEnum): """通知场景""" - TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户-临过期提醒")) - TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户-过期提醒")) + TENANT_USER_EXPIRING = EnumField("tenant_user_expiring", label=_("租户用户即将过期")) + TENANT_USER_EXPIRED = EnumField("tenant_user_expired", label=_("租户用户已过期")) DEFAULT_TENANT_USER_VALIDITY_PERIOD_CONFIG = { diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py index 36bdf868e..57a2468b3 100644 --- a/src/bk-user/bkuser/apps/tenant/models.py +++ b/src/bk-user/bkuser/apps/tenant/models.py @@ -81,6 +81,14 @@ class Meta: def account_expired_at_display(self) -> str: return datetime_to_display(self.account_expired_at) + @property + def real_phone(self) -> str: + return self.data_source_user.phone if self.is_inherited_phone else self.custom_phone + + @property + def real_email(self) -> str: + return self.data_source_user.email if self.is_inherited_email else self.custom_email + class TenantDepartment(TimestampedModel): """ diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index e6146020d..79a6e9b32 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -85,8 +85,6 @@ def _gen_tenant_user_expired_ctx(self) -> Dict[str, str]: class TenantUserValidityPeriodNotifier: """租户用户用户通知器,支持批量像用户发送某类信息""" - templates: List[NotificationTemplate] = [] - def __init__(self, tenant_id: str, scene: NotificationScene): self.tenant_id = tenant_id self.scene = scene @@ -127,19 +125,33 @@ def _send_notifications(self, user: TenantUser): self._send_sms(user, tmpl) def _send_email(self, user: TenantUser, tmpl: NotificationTemplate): + # 根据继承与否,获取真实邮箱 logger.info( - "send email to user %s, scene %s, title: %s", user.data_source_user.username, tmpl.scene, tmpl.title + "send email to user %s, email %s, scene %s, title: %s", + user.data_source_user.username, + user.real_email, + tmpl.scene, + tmpl.title, ) + email = user.real_email + if not email: + logger.info("user<%s> have no email, not to send_email", user.data_source_user.username) + return + content = self._render_tmpl(user, tmpl.content_html) - # 根据继承与否,获取真实邮箱 - email = user.data_source_user.email if user.is_inherited_email else user.custom_email cmsi.send_mail([email], tmpl.sender, tmpl.title, content) # type: ignore def _send_sms(self, user: TenantUser, tmpl: NotificationTemplate): - logger.info("send sms to user %s, scene %s", user.data_source_user.username, tmpl.scene) - content = self._render_tmpl(user, tmpl.content) + logger.info( + "send sms to user %s, phone %s, scene %s", user.data_source_user.username, user.real_phone, tmpl.scene + ) # 根据继承与否,获取真实手机号 - phone = user.data_source_user.phone if user.is_inherited_phone else user.custom_phone + phone = user.real_phone + if not phone: + logger.info("user<%s> have no phone number, not to send_sms", user.data_source_user.username) + return + + content = self._render_tmpl(user, tmpl.content) cmsi.send_sms([phone], content) def _render_tmpl(self, user: TenantUser, content: str) -> str: diff --git a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py b/src/bk-user/bkuser/apps/tenant/periodic_tasks.py deleted file mode 100644 index a4285f20d..000000000 --- a/src/bk-user/bkuser/apps/tenant/periodic_tasks.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- -""" -TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. -Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. -Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at http://opensource.org/licenses/MIT -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -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. -""" -import datetime -import logging - -from django.utils import timezone - -from bkuser.apps.tenant.constants import NotificationScene -from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig -from bkuser.apps.tenant.notifier import TenantUserValidityPeriodNotifier -from bkuser.celery import app -from bkuser.common.task import BaseTask - -logger = logging.getLogger(__name__) - - -@app.task(base=BaseTask, ignore_result=True) -def send_tenant_user_expiring_notification(): - """扫描全部租户用户,做临期通知""" - logger.info("[celery] receive period task:send_tenant_user_expiring_notification") - now = timezone.now() - # 获取 租户-未过期用户 映射 - tenant_ids = Tenant.objects.all().values_list("id", flat=True) - tenant_users_map = { - tenant_id: TenantUser.objects.filter( - account_expired_at__gt=now.replace(hour=0, minute=0, second=0), tenant_id=tenant_id - ) - for tenant_id in tenant_ids - } - - # 获取账号有效期-临期配置 - tenant_remind_before_expire_map = { - tenant_id: TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first().remind_before_expire - for tenant_id in tenant_ids - } - - for tenant_id, remind_before_expire in tenant_remind_before_expire_map.items(): - tenant_users = tenant_users_map[tenant_id] - - # 临1/7/15天过期 条件设置, 每个租户的设置都不一样 - should_notify_users = [] - for remain_days in remind_before_expire: - account_expired_date = now + datetime.timedelta(days=int(remain_days)) - should_notify_users += list( - tenant_users.filter( - account_expired_at__range=[ - account_expired_date.replace(hour=0, minute=0, second=0), - account_expired_date.replace(hour=23, minute=59, second=59, microsecond=999999), - ] - ) - ) - # 发送通知 - logger.info("going to notify expiring users in tenant{%s}, count: %s", tenant_id, len(should_notify_users)) - - if not should_notify_users: - return - - TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRING).send( - should_notify_users - ) - - -@app.task(base=BaseTask, ignore_result=True) -def send_tenant_user_expired_notification(): - """扫描全部租户用户,做过期通知""" - logger.info("[celery] receive period task:send_tenant_user_expired_notification") - - # 今日过期 - now = timezone.now() - # 获取 租户-过期用户 - tenant_ids = Tenant.objects.filter().values_list("id", flat=True) - tenant_user_map = { - tenant_id: TenantUser.objects.filter( - account_expired_at__range=[ - now.replace(hour=0, minute=0, second=0), - now.replace(hour=23, minute=59, second=59, microsecond=999999), - ], - tenant_id=tenant_id, - ) - for tenant_id in tenant_ids - } - - for tenant_id, tenant_users in tenant_user_map.items(): - # 发送过期通知 - should_notify_users = list(tenant_users) - logger.info("going to notify expired users in tenant{%s}, count: %s", tenant_id, len(should_notify_users)) - if not should_notify_users: - return - - TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRED).send( - should_notify_users - ) diff --git a/src/bk-user/bkuser/apps/tenant/tasks.py b/src/bk-user/bkuser/apps/tenant/tasks.py new file mode 100644 index 000000000..117c45587 --- /dev/null +++ b/src/bk-user/bkuser/apps/tenant/tasks.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available. +Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +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. +""" +import datetime +import logging +from typing import List + +from django.utils import timezone + +from bkuser.apps.tenant.constants import NotificationScene +from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig +from bkuser.apps.tenant.notifier import TenantUserValidityPeriodNotifier +from bkuser.celery import app +from bkuser.common.task import BaseTask + +logger = logging.getLogger(__name__) + + +@app.task(base=BaseTask, ignore_result=True) +def send_notifications(tenant_id: str, scene: NotificationScene, tenant_user_ids: List[str]): + # TODO: 后续考虑租户、用户状态等,冻结等非正常状态用户不通知 + users = TenantUser.objects.filter(id__in=tenant_user_ids) + logger.info( + "going to send notification for users. user_count=%s tenant=%s, scene=%s", len(users), tenant_id, scene + ) + try: + TenantUserValidityPeriodNotifier(tenant_id=tenant_id, scene=scene).send(users) + except Exception: + logger.exception("send notification failed, please check!") + + +@app.task(base=BaseTask, ignore_result=True) +def notify_expiring_tenant_user(): + """扫描全部租户用户,做即将过期通知""" + logger.info("[celery] receive period task:send_tenant_user_expiring_notification") + now = timezone.now() + + # 获取账号有效期-临期配置 + tenant_remind_before_expire_list = TenantUserValidityPeriodConfig.objects.all().values( + "tenant_id", "remind_before_expire" + ) + + for item in tenant_remind_before_expire_list: + tenant_id, remind_before_expire = item["tenant_id"], item["remind_before_expire"] + + # DateTimeField 使用__date__xx的orm查询,orm会将相应字段的进行转换时区. + # mysql 数据库需要加载系统的时区表 + # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#database-time-zone-definitions + # https://dev.mysql.com/doc/refman/8.2/en/mysql-tzinfo-to-sql.html + tenant_users = TenantUser.objects.filter(account_expired_at__date__gt=now.date(), tenant_id=tenant_id) + + # 临1/7/15天过期 条件设置, 每个租户的设置都不一样 + account_expired_date_list = [] + for remain_days in remind_before_expire: + account_expired_at = now + datetime.timedelta(days=int(remain_days)) + account_expired_date_list.append(account_expired_at.date()) + + should_notify_user_ids = list( + tenant_users.filter(account_expired_at__date__in=account_expired_date_list).values_list("id", flat=True) + ) + # 发送通知 + logger.info("going to notify expiring users in tenant{%s}, count: %s", tenant_id, len(should_notify_user_ids)) + + if not should_notify_user_ids: + continue + + send_notifications.delay( + tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRING, tenant_user_ids=should_notify_user_ids + ) + + +@app.task(base=BaseTask, ignore_result=True) +def notify_expired_tenant_user(): + """扫描全部租户用户,做过期通知""" + logger.info("[celery] receive period task:send_tenant_user_expired_notification") + + # 今日过期, 当前时间转换为实际时区的时间 + now = timezone.now() + + # 获取 租户-过期用户 + tenant_ids = Tenant.objects.all().values_list("id", flat=True) + + for tenant_id in tenant_ids: + # 发送过期通知 + should_notify_user_ids = list( + TenantUser.objects.filter( + account_expired_at__date=now.date(), + tenant_id=tenant_id, + ).values_list("id", flat=True) + ) + + logger.info("going to notify expired users in tenant{%s}, count: %s", tenant_id, len(should_notify_user_ids)) + if not should_notify_user_ids: + continue + + send_notifications.delay( + tenant_id=tenant_id, scene=NotificationScene.TENANT_USER_EXPIRED, tenant_user_ids=should_notify_user_ids + ) diff --git a/src/bk-user/bkuser/biz/data_source_organization.py b/src/bk-user/bkuser/biz/data_source_organization.py index 80e41242b..9c4e10fd5 100644 --- a/src/bk-user/bkuser/biz/data_source_organization.py +++ b/src/bk-user/bkuser/biz/data_source_organization.py @@ -14,7 +14,6 @@ from django.db import transaction from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from pydantic import BaseModel from bkuser.apps.data_source.models import ( @@ -25,7 +24,6 @@ DataSourceUserLeaderRelation, ) from bkuser.apps.tenant.models import Tenant, TenantUser, TenantUserValidityPeriodConfig -from bkuser.common.error_codes import error_codes from bkuser.utils.uuid import generate_uuid @@ -105,14 +103,9 @@ def create_user( DataSourceUserLeaderRelation.objects.bulk_create(user_leader_relation_objs) # 查询关联的租户 - tenant_id = data_source.owner_tenant_id - tenant = Tenant.objects.get(id=tenant_id) + tenant = Tenant.objects.get(id=data_source.owner_tenant_id) # 创建租户用户 - cfg = TenantUserValidityPeriodConfig.objects.filter(tenant_id=tenant_id).first() - if not cfg: - raise error_codes.OBJECT_NOT_FOUND.f(_("账户有效期配置丢失,请联系系统管理员")) - tenant_user = TenantUser( data_source_user=user, tenant=tenant, @@ -121,6 +114,7 @@ def create_user( ) # 根据配置初始化账号有效期 + cfg = TenantUserValidityPeriodConfig.objects.get(tenant_id=tenant.id) if cfg.enabled and cfg.validity_period > 0: tenant_user.account_expired_at = timezone.now() + datetime.timedelta(days=cfg.validity_period) # 入库 diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index ea6be8eef..e38006b7a 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -244,11 +244,11 @@ # 内置的周期任务 CELERYBEAT_SCHEDULE = { "periodic_notify_expiring_tenant_users": { - "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expiring_notification", + "task": "bkuser.apps.tenant.tasks.notify_expiring_tenant_user", "schedule": crontab(minute="0", hour="10"), # 每天10时执行 }, "periodic_notify_expired_tenant_users": { - "task": "bkuser.apps.tenant.periodic_tasks.send_tenant_user_expired_notification", + "task": "bkuser.apps.tenant.tasks.notify_expired_tenant_user", "schedule": crontab(minute="0", hour="10"), # 每天10时执行 }, } From 5c2de7e2f35f4f6b433e728a5dc2b0bb09bfdde5 Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 12:10:56 +0800 Subject: [PATCH 21/26] =?UTF-8?q?refactor:=20cr=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant_setting/views.py | 6 ++---- src/bk-user/bkuser/apps/tenant/notifier.py | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/views.py b/src/bk-user/bkuser/apis/web/tenant_setting/views.py index b88567af3..8be22b64c 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/views.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/views.py @@ -117,10 +117,8 @@ def delete(self, request, *args, **kwargs): class TenantUserValidityPeriodConfigRetrieveUpdateApi( ExcludePatchAPIViewMixin, CurrentUserTenantMixin, generics.RetrieveUpdateAPIView ): - queryset = TenantUserValidityPeriodConfig.objects.all() - def get_object(self): - queryset = self.filter_queryset(self.get_queryset()) + queryset = TenantUserValidityPeriodConfig.objects.all() filter_kwargs = {"tenant_id": self.get_current_tenant_id()} return get_object_or_404(queryset, **filter_kwargs) @@ -133,7 +131,7 @@ def get_object(self): ) def get(self, request, *args, **kwargs): instance = self.get_object() - return Response(TenantUserValidityPeriodConfigOutputSLZ(instance).data) + return Response(TenantUserValidityPeriodConfigOutputSLZ(instance=instance).data) @swagger_auto_schema( tags=["tenant-setting"], diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 79a6e9b32..631193ad7 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -98,7 +98,12 @@ def send(self, users: List[TenantUser]) -> None: self._send_notifications(u) # TODO 细化异常处理 except Exception: - logger.exception("send notification failed") + logger.exception( + "send notification failed, tenant: %s, scene:%s, tenant_user_id: %s", + self.tenant_id, + self.scene, + u.id, + ) def _get_templates_with_scene(self, scene: NotificationScene) -> List[NotificationTemplate]: """根据场景以及插件配置中设置的通知方式,获取需要发送通知的模板""" From f5c097c2772462a932d5859dab81c63b90a61fbe Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 16:22:38 +0800 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bk-user/bkuser/apps/tenant/tasks.py b/src/bk-user/bkuser/apps/tenant/tasks.py index 117c45587..c23d36e56 100644 --- a/src/bk-user/bkuser/apps/tenant/tasks.py +++ b/src/bk-user/bkuser/apps/tenant/tasks.py @@ -51,7 +51,8 @@ def notify_expiring_tenant_user(): tenant_id, remind_before_expire = item["tenant_id"], item["remind_before_expire"] # DateTimeField 使用__date__xx的orm查询,orm会将相应字段的进行转换时区. - # mysql 数据库需要加载系统的时区表 + # mysql 数据库需要加载系统的时区表, 可以通过下面的sql,来确认是否加载了 + # SELECT CONVERT_TZ("2023-11-18 06:38:32.000000", 'UTC', 'Asia/Shanghai') # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#database-time-zone-definitions # https://dev.mysql.com/doc/refman/8.2/en/mysql-tzinfo-to-sql.html tenant_users = TenantUser.objects.filter(account_expired_at__date__gt=now.date(), tenant_id=tenant_id) From 0e51bae003bd077d5d20368df5c642e684d99473 Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 17:43:04 +0800 Subject: [PATCH 23/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apis/web/tenant_setting/serializers.py | 2 +- src/bk-user/bkuser/apps/tenant/notifier.py | 3 +-- src/bk-user/bkuser/apps/tenant/tasks.py | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 974b40775..71d921bd5 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -177,7 +177,7 @@ class NotificationTemplatesInputSLZ(serializers.Serializer): class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): enabled = serializers.BooleanField(help_text="是否启用账户有效期") validity_period = serializers.IntegerField(help_text="账户有效期,单位:天") - remind_before_expire = serializers.ListField(help_text="临过期提醒时间") + remind_before_expire = serializers.ListField(help_text="临过期提醒时间", child=serializers.IntegerField()) enabled_notification_methods = serializers.ListField( help_text="通知方式", child=serializers.ChoiceField(choices=NotificationMethod.get_choices()), diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 631193ad7..714be22c0 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -99,10 +99,9 @@ def send(self, users: List[TenantUser]) -> None: # TODO 细化异常处理 except Exception: logger.exception( - "send notification failed, tenant: %s, scene:%s, tenant_user_id: %s", + "send notification failed, tenant: %s, scene: %s", self.tenant_id, self.scene, - u.id, ) def _get_templates_with_scene(self, scene: NotificationScene) -> List[NotificationTemplate]: diff --git a/src/bk-user/bkuser/apps/tenant/tasks.py b/src/bk-user/bkuser/apps/tenant/tasks.py index c23d36e56..2dd34d2a2 100644 --- a/src/bk-user/bkuser/apps/tenant/tasks.py +++ b/src/bk-user/bkuser/apps/tenant/tasks.py @@ -50,11 +50,6 @@ def notify_expiring_tenant_user(): for item in tenant_remind_before_expire_list: tenant_id, remind_before_expire = item["tenant_id"], item["remind_before_expire"] - # DateTimeField 使用__date__xx的orm查询,orm会将相应字段的进行转换时区. - # mysql 数据库需要加载系统的时区表, 可以通过下面的sql,来确认是否加载了 - # SELECT CONVERT_TZ("2023-11-18 06:38:32.000000", 'UTC', 'Asia/Shanghai') - # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#database-time-zone-definitions - # https://dev.mysql.com/doc/refman/8.2/en/mysql-tzinfo-to-sql.html tenant_users = TenantUser.objects.filter(account_expired_at__date__gt=now.date(), tenant_id=tenant_id) # 临1/7/15天过期 条件设置, 每个租户的设置都不一样 From aa7861b04a2970c7c9a68ce3d9ffc7ec7efb61fa Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 19:43:38 +0800 Subject: [PATCH 24/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apis/web/tenant_setting/serializers.py | 5 ++++- src/bk-user/bkuser/apps/tenant/notifier.py | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py index 71d921bd5..cbeed75c0 100644 --- a/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant_setting/serializers.py @@ -177,7 +177,10 @@ class NotificationTemplatesInputSLZ(serializers.Serializer): class TenantUserValidityPeriodConfigInputSLZ(serializers.Serializer): enabled = serializers.BooleanField(help_text="是否启用账户有效期") validity_period = serializers.IntegerField(help_text="账户有效期,单位:天") - remind_before_expire = serializers.ListField(help_text="临过期提醒时间", child=serializers.IntegerField()) + remind_before_expire = serializers.ListField( + help_text="临过期提醒时间", + child=serializers.IntegerField(min_value=1), + ) enabled_notification_methods = serializers.ListField( help_text="通知方式", child=serializers.ChoiceField(choices=NotificationMethod.get_choices()), diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 714be22c0..18fec7450 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -93,16 +93,17 @@ def __init__(self, tenant_id: str, scene: NotificationScene): def send(self, users: List[TenantUser]) -> None: """根据配置,发送对应的通知信息""" - try: - for u in users: + for u in users: + try: self._send_notifications(u) - # TODO 细化异常处理 - except Exception: - logger.exception( - "send notification failed, tenant: %s, scene: %s", - self.tenant_id, - self.scene, - ) + # TODO 细化异常处理 + except Exception: # noqa: PERF203 + logger.exception( + "send notification failed, tenant: %s, scene: %s, tenant_user: %s", + self.tenant_id, + self.scene, + u.id, + ) def _get_templates_with_scene(self, scene: NotificationScene) -> List[NotificationTemplate]: """根据场景以及插件配置中设置的通知方式,获取需要发送通知的模板""" From bfd9e53ef51bf3779eee91427efb1571dcd1bc99 Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 20:06:45 +0800 Subject: [PATCH 25/26] =?UTF-8?q?refactor:=20cr=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bk-user/bkuser/apps/tenant/notifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bk-user/bkuser/apps/tenant/notifier.py b/src/bk-user/bkuser/apps/tenant/notifier.py index 18fec7450..1064149e4 100644 --- a/src/bk-user/bkuser/apps/tenant/notifier.py +++ b/src/bk-user/bkuser/apps/tenant/notifier.py @@ -96,7 +96,7 @@ def send(self, users: List[TenantUser]) -> None: for u in users: try: self._send_notifications(u) - # TODO 细化异常处理 + # TODO 细化异常处理 except Exception: # noqa: PERF203 logger.exception( "send notification failed, tenant: %s, scene: %s, tenant_user: %s", From a33d5edcd3270ad4778e20cc6614d839c3ff0b29 Mon Sep 17 00:00:00 2001 From: nero Date: Mon, 13 Nov 2023 20:20:03 +0800 Subject: [PATCH 26/26] feature: add migrations --- ...{0006_auto_20231108_1207.py => 0003_auto_20231113_2017.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/bk-user/bkuser/apps/tenant/migrations/{0006_auto_20231108_1207.py => 0003_auto_20231113_2017.py} (94%) diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py b/src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py similarity index 94% rename from src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py rename to src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py index 73d7ac5e7..011a8acbd 100644 --- a/src/bk-user/bkuser/apps/tenant/migrations/0006_auto_20231108_1207.py +++ b/src/bk-user/bkuser/apps/tenant/migrations/0003_auto_20231113_2017.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.20 on 2023-11-08 04:07 +# Generated by Django 3.2.20 on 2023-11-13 12:17 from django.db import migrations, models import django.db.models.deletion @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('tenant', '0005_builtin_user_fields'), + ('tenant', '0002_init_builtin_user_fields'), ] operations = [