diff --git a/dbm-ui/backend/configuration/models/dba.py b/dbm-ui/backend/configuration/models/dba.py index fa3689f058..04e0c65bf4 100644 --- a/dbm-ui/backend/configuration/models/dba.py +++ b/dbm-ui/backend/configuration/models/dba.py @@ -60,3 +60,11 @@ def get_biz_db_type_admins(cls, bk_biz_id: int, db_type: str) -> List[str]: if db_type == admin["db_type"]: return admin["users"] or DEFAULT_DB_ADMINISTRATORS return DEFAULT_DB_ADMINISTRATORS + + @classmethod + def get_dba_for_db_type(cls, bk_biz_id: int, db_type: str) -> List[str]: + """获取主dba、备dba、二线dba人员""" + dba_list = cls.list_biz_admins(bk_biz_id) + dba_content = next((dba for dba in dba_list if dba["db_type"] == db_type), {"users": []}) + users = dba_content.get("users", []) + return users[:1], users[1:2], users[2:] diff --git a/dbm-ui/backend/ticket/filters.py b/dbm-ui/backend/ticket/filters.py index 7f783e2ca5..7a15945a23 100644 --- a/dbm-ui/backend/ticket/filters.py +++ b/dbm-ui/backend/ticket/filters.py @@ -24,6 +24,7 @@ class TicketListFilter(filters.FilterSet): cluster = filters.CharFilter(field_name="cluster", method="filter_cluster", label=_("集群域名")) todo = filters.CharFilter(field_name="todo", method="filter_todo", label=_("代办状态")) ordering = filters.CharFilter(field_name="ordering", method="order_ticket", label=_("排序字段")) + is_assist = filters.BooleanFilter(field_name="is_assist", method="filter_is_assist", label=_("是否协助")) class Meta: model = Ticket @@ -47,11 +48,21 @@ def filter_ids(self, queryset, name, value): def filter_todo(self, queryset, name, value): user = self.request.user.username if value == "running": - todo_filter = Q(todo_of_ticket__operators__contains=user, todo_of_ticket__status__in=TODO_RUNNING_STATUS) + todo_filter = Q( + Q(todo_of_ticket__operators__contains=user) | Q(todo_of_ticket__helpers__contains=user), + todo_of_ticket__status__in=TODO_RUNNING_STATUS, + ) else: todo_filter = Q(todo_of_ticket__done_by=user) return queryset.filter(todo_filter).distinct() + def filter_is_assist(self, queryset, name, value): + user = self.request.user.username + # 根据 value 的值选择不同的字段 + field = "helpers" if value else "operators" + todo_filter = Q(**{f"todo_of_ticket__{field}__contains": user}, todo_of_ticket__status__in=TODO_RUNNING_STATUS) + return queryset.filter(todo_filter).distinct() + def filter_status(self, queryset, name, value): status = value.split(",") status_filter = Q() diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py index 61152eeb40..d29f194deb 100644 --- a/dbm-ui/backend/ticket/handler.py +++ b/dbm-ui/backend/ticket/handler.py @@ -40,7 +40,6 @@ from backend.ticket.exceptions import TicketFlowsConfigException from backend.ticket.flow_manager.manager import TicketFlowManager from backend.ticket.models import Flow, Ticket, TicketFlowsConfig, Todo -from backend.ticket.serializers import TodoSerializer from backend.ticket.todos import BaseTodoContext, TodoActorFactory from backend.ticket.todos.itsm_todo import ItsmTodoContext @@ -287,6 +286,7 @@ def batch_process_todo(cls, user, action, operations): @param action 动作 @param operations: todo列表,每个item包含todo id和params """ + from backend.ticket.serializers import TodoSerializer results = [] for operation in operations: diff --git a/dbm-ui/backend/ticket/migrations/0013_todo_helpers.py b/dbm-ui/backend/ticket/migrations/0013_todo_helpers.py new file mode 100644 index 0000000000..e5dec1a440 --- /dev/null +++ b/dbm-ui/backend/ticket/migrations/0013_todo_helpers.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-01-03 02:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ticket", "0012_alter_ticket_remark"), + ] + + operations = [ + migrations.AddField( + model_name="todo", + name="helpers", + field=models.JSONField(default=list, verbose_name="协助人"), + ), + ] diff --git a/dbm-ui/backend/ticket/models/todo.py b/dbm-ui/backend/ticket/models/todo.py index 9516896f37..6638c1d33c 100644 --- a/dbm-ui/backend/ticket/models/todo.py +++ b/dbm-ui/backend/ticket/models/todo.py @@ -39,39 +39,52 @@ def exist_unfinished(self): def get_operators(self, todo_type, ticket, operators): # 获得提单人,dba,业务协助人. TODO: 后续还会细分主、备、二线DBA,以及明确区分协助人角色 creator = [ticket.creator] - dba = DBAdministrator.get_biz_db_type_admins(ticket.bk_biz_id, ticket.group) + # dba = DBAdministrator.get_biz_db_type_admins(ticket.bk_biz_id, ticket.group) + dba, second_dba, other_dba = DBAdministrator.get_dba_for_db_type(ticket.bk_biz_id, ticket.group) biz_helpers = BizSettings.get_assistance(ticket.bk_biz_id) # 构造单据状态与处理人之间的对应关系 # - 审批中:提单人可撤销,dba可处理, # 考虑某些单据审批人是特定配置(数据导出 -- 运维审批),所以从ItsmBuilder获得审批人 - # - 待执行:提单人 + 单据协助人 - # - 待继续:dba + 提单人 + 单据协助人 - # - 待补货:dba + 提单人 + 单据协助人 - # - 已失败:dba + 提单人 + 单据协助人 + # - 待执行:operators[提单人] + helpers[单据协助人] + # - 待继续:operators[提单人 + dba] + helpers[单据协助人 + second_dba + other_dba] + # - 待补货:operators[提单人 + dba] + helpers[单据协助人 + second_dba + other_dba] + # - 已失败:operators[提单人 + dba] + helpers[单据协助人 + second_dba + other_dba] itsm_builder = BuilderFactory.get_builder_cls(ticket.ticket_type).itsm_flow_builder(ticket) + itsm_operators = itsm_builder.get_approvers().split(",") todo_operators_map = { - TodoType.ITSM: itsm_builder.get_approvers().split(","), - TodoType.APPROVE: creator + biz_helpers, - TodoType.INNER_APPROVE: dba + creator + biz_helpers, - TodoType.RESOURCE_REPLENISH: dba + creator + biz_helpers, - TodoType.INNER_FAILED: dba + creator + biz_helpers, + TodoType.ITSM: itsm_operators[:1], + TodoType.APPROVE: creator, + TodoType.INNER_APPROVE: creator + dba, + TodoType.RESOURCE_REPLENISH: creator + dba, + TodoType.INNER_FAILED: creator + dba, + } + todo_helpers_map = { + TodoType.ITSM: itsm_operators[1:], + TodoType.APPROVE: biz_helpers, + TodoType.INNER_APPROVE: biz_helpers + second_dba + other_dba, + TodoType.RESOURCE_REPLENISH: biz_helpers + second_dba + other_dba, + TodoType.INNER_FAILED: biz_helpers + second_dba + other_dba, } # 按照顺序去重 operators = list(dict.fromkeys(operators + todo_operators_map.get(todo_type, []))) - return operators + helpers = [item for item in todo_helpers_map.get(todo_type, []) if item not in operators] + return creator, biz_helpers, helpers, operators def create(self, **kwargs): - operators = self.get_operators(kwargs["type"], kwargs["ticket"], kwargs.get("operators", [])) + creator, biz_helpers, helpers, operators = self.get_operators( + kwargs["type"], kwargs["ticket"], kwargs.get("operators", []) + ) kwargs["operators"] = operators + kwargs["helpers"] = helpers todo = super().create(**kwargs) send_msg_for_flow.apply_async( kwargs={ "flow_id": todo.flow.id, "flow_msg_type": FlowMsgType.TODO.value, "flow_status": FlowMsgStatus.UNCONFIRMED.value, - "processor": ",".join(todo.operators), - "receiver": todo.creator, + "processor": ",".join(todo.operators + todo.helpers), + "receiver": ",".join(creator + biz_helpers), } ) return todo @@ -86,6 +99,7 @@ class Todo(AuditedModel): flow = models.ForeignKey("Flow", help_text=_("关联流程任务"), related_name="todo_of_flow", on_delete=models.CASCADE) ticket = models.ForeignKey("Ticket", help_text=_("关联工单"), related_name="todo_of_ticket", on_delete=models.CASCADE) operators = models.JSONField(_("待办人"), default=list) + helpers = models.JSONField(_("协助人"), default=list) type = models.CharField( _("待办类型"), choices=TodoType.get_choices(), diff --git a/dbm-ui/backend/ticket/serializers.py b/dbm-ui/backend/ticket/serializers.py index 285e244d82..4678ff380a 100644 --- a/dbm-ui/backend/ticket/serializers.py +++ b/dbm-ui/backend/ticket/serializers.py @@ -96,6 +96,7 @@ class TicketSerializer(AuditedSerializer, serializers.ModelSerializer): details = TicketDetailsSerializer(help_text=_("单据详情")) # 额外补充展示字段 todo_operators = serializers.SerializerMethodField(help_text=_("处理人列表")) + todo_helpers = serializers.SerializerMethodField(help_text=_("协助人列表")) status = serializers.SerializerMethodField(help_text=_("状态"), read_only=True) status_display = serializers.SerializerMethodField(help_text=_("状态名称")) ticket_type_display = serializers.SerializerMethodField(help_text=_("单据类型名称")) @@ -125,8 +126,12 @@ def get_todo_operators(self, obj): obj.running_todos = [todo for todo in obj.todo_of_ticket.all() if todo.status == TodoStatus.TODO] return obj.running_todos[0].operators if obj.running_todos else [] + def get_todo_helpers(self, obj): + obj.running_todo_helpers = [todo for todo in obj.todo_of_ticket.all() if todo.status == TodoStatus.TODO] + return obj.running_todo_helpers[0].helpers if obj.running_todo_helpers else [] + def get_status(self, obj): - if obj.status == TicketStatus.RUNNING and obj.running_todos: + if obj.status == TicketStatus.RUNNING and (obj.running_todos or obj.running_todo_helpers): obj.status = TicketStatus.INNER_TODO return obj.status @@ -230,7 +235,6 @@ class TodoSerializer(serializers.ModelSerializer): 单据序列化 """ - operators = serializers.JSONField(help_text=_("待办人列表")) cost_time = serializers.SerializerMethodField(help_text=_("耗时")) def get_cost_time(self, obj): diff --git a/dbm-ui/backend/ticket/todos/__init__.py b/dbm-ui/backend/ticket/todos/__init__.py index 35412d6cc3..03f6de1564 100644 --- a/dbm-ui/backend/ticket/todos/__init__.py +++ b/dbm-ui/backend/ticket/todos/__init__.py @@ -58,7 +58,7 @@ def process(self, username, action, params): return # 允许超级用户和操作人确认 is_superuser = User.objects.get(username=username).is_superuser - if not is_superuser and username not in self.todo.operators: + if not is_superuser and username not in self.todo.operators + self.todo.helpers: raise TodoWrongOperatorException(_("{}不在处理人: {}中,无法处理").format(username, self.todo.operators)) # 执行确认操作 diff --git a/dbm-ui/backend/ticket/views.py b/dbm-ui/backend/ticket/views.py index da5564f73c..98b63e7ef8 100644 --- a/dbm-ui/backend/ticket/views.py +++ b/dbm-ui/backend/ticket/views.py @@ -274,7 +274,9 @@ def retrieve(self, request, *args, **kwargs): def list(self, request, *args, **kwargs): resp = super().list(request, *args, **kwargs) # 补充单据关联信息 - resp.data["results"] = TicketHandler.add_related_object(resp.data["results"]) + resp.data["results"] = TicketHandler.add_related_object( + list(filter(lambda x: x is not None, resp.data["results"])) + ) # 如果是查询自身单据(self_manage),则不进行鉴权 skip_iam = "self_manage" in request.query_params resp.data["results"] = [{"skip_iam": skip_iam, **t} for t in resp.data["results"]] @@ -447,29 +449,43 @@ def get_tickets_count(self, request, *args, **kwargs): """ user = request.user.username tickets = self._get_self_manage_tickets(request.user) - count_map = {count_type: 0 for count_type in CountType.get_values()} + exclude_values = {"MY_APPROVE", "SELF_MANAGE", "DONE"} + + # 初始化 count_map + def initialize_count_map(): + return {count_type: 0 for count_type in CountType.get_values() if count_type not in exclude_values} + results = {} + + # 通用的函数来计算待办和协助状态 + def calculate_status_count(field_name, relation_name): + status_counts = ( + tickets.filter( + status__in=TICKET_TODO_STATUS_SET, + **{f"{relation_name}__{field_name}__contains": user}, + **{f"{relation_name}__status__in": TODO_RUNNING_STATUS}, + ) + .distinct() + .values_list("status", flat=True) + ) + count_map = initialize_count_map() + for sts, count in Counter(status_counts).items(): + sts = CountType.INNER_TODO.value if sts == "RUNNING" else sts + count_map[sts] = count + return count_map + + # 计算我的代办 + results["Pending"] = calculate_status_count("operators", "todo_of_ticket") + # 计算我的协助 + results["to_help"] = calculate_status_count("helpers", "todo_of_ticket") # 我负责的业务 - count_map[CountType.SELF_MANAGE] = tickets.count() + results[CountType.SELF_MANAGE] = tickets.count() # 我的申请 - count_map[CountType.MY_APPROVE] = tickets.filter(creator=user).count() - # 我的代办 - todo_status = ( - tickets.filter( - status__in=TICKET_TODO_STATUS_SET, - todo_of_ticket__operators__contains=user, - todo_of_ticket__status__in=TODO_RUNNING_STATUS, - ) - .distinct() - .values_list("status", flat=True) - ) - for sts, count in Counter(todo_status).items(): - sts = CountType.INNER_TODO.value if sts == "RUNNING" else sts - count_map[sts] = count + results[CountType.MY_APPROVE] = tickets.filter(creator=user).count() # 我的已办 - count_map[CountType.DONE] = tickets.filter(todo_of_ticket__done_by=user).count() + results[CountType.DONE] = tickets.filter(todo_of_ticket__done_by=user).count() - return Response(count_map) + return Response(results) @common_swagger_auto_schema( operation_summary=_("查询集群变更单据事件"),