From 1f2c70fe8ee433ee0a32d2a4474f37a75d862056 Mon Sep 17 00:00:00 2001
From: iSecloud <869820505@qq.com>
Date: Fri, 3 Jan 2025 20:44:55 +0800
Subject: [PATCH] =?UTF-8?q?feat(backend):=20bkchat=E4=B8=93=E5=B1=9Eproces?=
=?UTF-8?q?s=20todo=20#8755?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
dbm-ui/backend/core/notify/handlers.py | 59 +++++++++++--------
dbm-ui/backend/core/notify/template.py | 12 ++--
.../backend/db_services/plugin/constants.py | 2 +-
.../db_services/plugin/ticket/serializers.py | 11 +++-
.../db_services/plugin/ticket/views.py | 42 ++++++++++++-
.../collections/mysql/fake_semantic_check.py | 2 +-
dbm-ui/backend/ticket/exceptions.py | 8 ++-
dbm-ui/backend/ticket/flow_manager/manager.py | 19 ++++--
.../backend/ticket/flow_manager/resource.py | 2 +
dbm-ui/backend/ticket/handler.py | 11 +++-
dbm-ui/backend/ticket/todos/__init__.py | 4 +-
dbm-ui/backend/ticket/todos/pipeline_todo.py | 2 +-
dbm-ui/backend/utils/time.py | 2 +-
13 files changed, 126 insertions(+), 50 deletions(-)
diff --git a/dbm-ui/backend/core/notify/handlers.py b/dbm-ui/backend/core/notify/handlers.py
index d215b3b161..20b8d38a51 100644
--- a/dbm-ui/backend/core/notify/handlers.py
+++ b/dbm-ui/backend/core/notify/handlers.py
@@ -24,12 +24,12 @@
from backend.core.notify.exceptions import NotifyBaseException
from backend.core.notify.template import FAILED_TEMPLATE, FINISHED_TEMPLATE, TERMINATE_TEMPLATE, TODO_TEMPLATE
from backend.db_meta.models import AppCache
+from backend.exceptions import ApiResultError
from backend.ticket.builders import BuilderFactory
-from backend.ticket.constants import TicketStatus, TicketType
+from backend.ticket.constants import TicketStatus, TicketType, TodoStatus
from backend.ticket.models import Flow, Ticket
from backend.ticket.todos import ActionType
from backend.utils.cache import func_cache_decorator
-from backend.utils.time import datetime2str
logger = logging.getLogger("root")
@@ -78,20 +78,25 @@ def get_actions(msg_type, ticket):
"""获取bkchat操作按钮"""
if ticket.status not in [TicketStatus.APPROVE, TicketStatus.TODO]:
return []
+
+ todo = ticket.todo_of_ticket.filter(status=TodoStatus.TODO).first()
+ if not todo:
+ return []
+
# 增加回调按钮,执行和终止
agree_action = {
"name": _("同意") if ticket.status == TicketStatus.APPROVE else _("确认执行"),
"color": "green",
- "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/",
- "callback_data": {"action": ActionType.APPROVE.value, "ticket_ids": [ticket.id]},
+ "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/",
+ "callback_data": {"action": ActionType.APPROVE.value, "todo_id": todo.id, "params": {}},
}
refuse_action = {
"name": _("拒绝") if ticket.status == TicketStatus.APPROVE else _("终止单据"),
"color": "red",
- "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/",
+ "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/",
"callback_data": {
"action": ActionType.TERMINATE.value,
- "ticket_ids": [ticket.id],
+ "todo_id": todo.id,
"params": {"remark": _("使用「蓝鲸审批助手」终止单据")},
},
}
@@ -178,6 +183,8 @@ def send_mail(self, sender: str = None, cc: list = None):
kwargs.update(sender=sender)
if cc:
kwargs.update(cc__username=",".join(cc))
+ # 邮件的换行要用
的html
+ self.content = self.content.replace("\n", "
")
self._cmsi_send_msg(MsgType.MAIL, **kwargs)
def send_voice(self):
@@ -193,7 +200,9 @@ def send_rtx(self):
self._cmsi_send_msg(MsgType.RTX.value)
def send_sms(self):
- """发送企微消息"""
+ """发送短信消息"""
+ # 短信消息没有标题参数,直接把标题和内容放在一起
+ self.content = f"{self.title}\n{self.content}"
self._cmsi_send_msg(MsgType.SMS.value)
def send_wecom_robot(self):
@@ -238,7 +247,11 @@ def __init__(self, ticket_id: int, flow_id: int = None):
def get_support_msg_types(cls):
# 获取当前环境下支持的通知类型
# 所有的拓展方式都需要接入CMSI,所以直接返回CMSI支持方式即可
- return CmsiApi.get_msg_type()
+ # 暂不暴露微信的通知方式
+ msg_types = CmsiApi.get_msg_type()
+ msg_type_map = {msg["type"]: msg for msg in msg_types}
+ msg_type_map[MsgType.WEIXIN.value]["is_active"] = False
+ return list(msg_type_map.values())
def get_notify_class(self, msg_type: str):
# 根据通知类型获取通知类,以及通知所需的上下文
@@ -253,15 +266,17 @@ def get_receivers(self):
biz_helpers = BizSettings.get_assistance(self.bk_biz_id)
creator = [self.ticket.creator]
# 待审批:审批人
- # 待执行、待补货、待确认、已失败、已完成、已终止: 提单人、协助人
+ # 待执行、待补货、待确认、已失败、已完成、已终止:提单人、协助人
# 暂不通知DBA
if self.phase in [TicketStatus.PENDING]:
- return creator
+ receivers = creator
elif self.phase in [TicketStatus.APPROVE]:
itsm_builder = BuilderFactory.get_builder_cls(self.ticket.ticket_type).itsm_flow_builder(self.ticket)
- return itsm_builder.get_approvers().split(",")
+ receivers = itsm_builder.get_approvers().split(",")
else:
- return creator + biz_helpers
+ receivers = creator + biz_helpers
+ # 去重后返回
+ return list(dict.fromkeys(receivers))
def render_msg_template(self, msg_type: str):
# 获取标题,在群机器人通知则加上@人
@@ -289,8 +304,8 @@ def render_msg_template(self, msg_type: str):
"cluster_domains": ",".join(self.clusters),
"remark": self.ticket.remark,
"creator": self.ticket.creator,
- "submit_time": datetime2str(self.ticket.create_at),
- "update_time": datetime2str(self.ticket.update_at),
+ "submit_time": self.ticket.create_at.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"),
+ "update_time": self.ticket.update_at.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"),
"status": TicketStatus.get_choice_label(self.phase),
"operators": ",".join(self.ticket.get_current_operators()),
"detail_address": self.ticket.url,
@@ -325,17 +340,13 @@ def send_msg(self):
if msg_type == MsgType.WECOM_ROBOT:
self.receivers = send_msg_config.get(MsgType.WECOM_ROBOT.value, [])
- notify_class(title, content, self.receivers).send_msg(msg_type, context=context)
+ try:
+ notify_class(title, content, self.receivers).send_msg(msg_type, context=context)
+ except (ApiResultError, Exception) as e:
+ logger.error(_("[{}]消息发送失败,错误信息: {}").format(MsgType.get_choice_label(msg_type), e))
@shared_task
-def send_msg(ticket_id: int, flow_id: int = None, raise_exception: bool = False):
+def send_msg(ticket_id: int, flow_id: int = None):
# 可异步发送消息,非阻塞路径默认不抛出异常
- try:
- NotifyAdapter(ticket_id, flow_id).send_msg()
- except Exception as e:
- err_msg = _("消息发送失败,错误信息:{}").format(e)
- if not raise_exception:
- logger.error(err_msg)
- else:
- raise NotifyBaseException(err_msg)
+ NotifyAdapter(ticket_id, flow_id).send_msg()
diff --git a/dbm-ui/backend/core/notify/template.py b/dbm-ui/backend/core/notify/template.py
index e9aee22e23..d1b60f9453 100644
--- a/dbm-ui/backend/core/notify/template.py
+++ b/dbm-ui/backend/core/notify/template.py
@@ -16,10 +16,10 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
- 所属业务: {{biz_name}}
+ 业务: {{biz_name}}
域名: {{cluster_domains}}
备注: {{remark}}
- 处理人: {{operators}}
+ 当前处理人: {{operators}}
查看详情: {{detail_address}}\
"""
)
@@ -29,7 +29,7 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
- 所属业务: {{biz_name}}
+ 业务: {{biz_name}}
域名: {{cluster_domains}}
完成时间: {{update_time}}
查看详情: {{detail_address}}\
@@ -41,10 +41,10 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
- 所属业务: {{biz_name}}
+ 业务: {{biz_name}}
域名: {{cluster_domains}}
失败时间: {{update_time}}
- 处理人: {{operators}}
+ 当前当前处理人: {{operators}}
查看详情: {{detail_address}}\
"""
)
@@ -54,7 +54,7 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
- 所属业务: {{biz_name}}
+ 业务: {{biz_name}}
域名: {{cluster_domains}}
终止时间: {{update_time}}
终止原因: {{terminate_reason}}
diff --git a/dbm-ui/backend/db_services/plugin/constants.py b/dbm-ui/backend/db_services/plugin/constants.py
index bc29cd9419..f3868d829c 100644
--- a/dbm-ui/backend/db_services/plugin/constants.py
+++ b/dbm-ui/backend/db_services/plugin/constants.py
@@ -9,4 +9,4 @@
specific language governing permissions and limitations under the License.
"""
-SWAGGER_TAG = "plugin"
+SWAGGER_TAG = "OpenAPI"
diff --git a/dbm-ui/backend/db_services/plugin/ticket/serializers.py b/dbm-ui/backend/db_services/plugin/ticket/serializers.py
index 92b0a4c725..ba1272af61 100644
--- a/dbm-ui/backend/db_services/plugin/ticket/serializers.py
+++ b/dbm-ui/backend/db_services/plugin/ticket/serializers.py
@@ -12,8 +12,17 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
-from backend.ticket.serializers import BatchTicketOperateSerializer
+from backend.ticket.serializers import BatchTicketOperateSerializer, TodoOperateSerializer
class OpenAPIBatchTicketOperateSerializer(BatchTicketOperateSerializer):
username = serializers.CharField(help_text=_("操作者"))
+
+
+class OpenAPIBkChatProcessTodoSerializer(TodoOperateSerializer):
+ username = serializers.CharField(help_text=_("操作者"))
+
+
+class OpenAPIBkChatProcessTodoResponseSerializer(serializers.Serializer):
+ response_msg = serializers.CharField(help_text=_("返回信息"))
+ response_color = serializers.CharField(help_text=_("按钮颜色"))
diff --git a/dbm-ui/backend/db_services/plugin/ticket/views.py b/dbm-ui/backend/db_services/plugin/ticket/views.py
index c0f9d74e1d..85f1d55ba8 100644
--- a/dbm-ui/backend/db_services/plugin/ticket/views.py
+++ b/dbm-ui/backend/db_services/plugin/ticket/views.py
@@ -17,10 +17,17 @@
from rest_framework.response import Response
from backend.db_services.plugin.constants import SWAGGER_TAG
-from backend.db_services.plugin.ticket.serializers import OpenAPIBatchTicketOperateSerializer
+from backend.db_services.plugin.ticket.serializers import (
+ OpenAPIBatchTicketOperateSerializer,
+ OpenAPIBkChatProcessTodoResponseSerializer,
+ OpenAPIBkChatProcessTodoSerializer,
+)
from backend.db_services.plugin.view import BaseOpenAPIViewSet
+from backend.ticket.constants import TodoStatus, TodoType
+from backend.ticket.exceptions import TodoDuplicateProcessException
from backend.ticket.handler import TicketHandler
-from backend.ticket.serializers import TodoSerializer
+from backend.ticket.models import Todo
+from backend.ticket.todos import TodoActorFactory
logger = logging.getLogger("root")
@@ -29,10 +36,39 @@ class TicketViewSet(BaseOpenAPIViewSet):
@swagger_auto_schema(
operation_summary=_("批量单据待办处理"),
request_body=OpenAPIBatchTicketOperateSerializer(),
- responses={status.HTTP_200_OK: TodoSerializer(many=True)},
tags=[SWAGGER_TAG],
)
@action(methods=["POST"], detail=False, serializer_class=OpenAPIBatchTicketOperateSerializer)
def batch_process_ticket(self, request, *args, **kwargs):
params = self.params_validate(self.get_serializer_class())
return Response(TicketHandler.batch_process_ticket(**params))
+
+ @swagger_auto_schema(
+ operation_summary=_("待办处理(bkchat专属)"),
+ request_body=OpenAPIBkChatProcessTodoSerializer(),
+ responses={status.HTTP_200_OK: OpenAPIBkChatProcessTodoResponseSerializer()},
+ tags=[SWAGGER_TAG],
+ )
+ @action(methods=["POST"], detail=False, serializer_class=OpenAPIBkChatProcessTodoSerializer)
+ def bkchat_process_todo(self, request, *args, **kwargs):
+ """
+ bkchat专属的待办处理,区别主要是返回结构不同
+ """
+ params = self.params_validate(self.get_serializer_class())
+
+ todo = Todo.objects.get(id=params["todo_id"])
+ if todo.type not in [TodoType.ITSM, TodoType.APPROVE]:
+ return Response({"response_msg": _("暂不支持该类型{}todo的处理").fromat(todo.type), "response_color": "red"})
+
+ # 确认todo,忽略重复操作
+ try:
+ TodoActorFactory.actor(todo).process(params["username"], params["action"], params["params"])
+ except TodoDuplicateProcessException:
+ pass
+
+ # 根据操作类型获取文案和按钮颜色
+ todo.refresh_from_db()
+ if todo.status == TodoStatus.DONE_FAILED:
+ return Response({"response_msg": _("{} 已终止").format(todo.done_by), "response_color": "red"})
+ elif todo.status == TodoStatus.DONE_SUCCESS:
+ return Response({"response_msg": _("{} 已确认").format(todo.done_by), "response_color": "green"})
diff --git a/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py b/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py
index 4c938297c0..180e568f74 100644
--- a/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py
+++ b/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py
@@ -25,7 +25,7 @@ def _execute(self, data, parent_data, callback=None) -> bool:
kwargs = data.get_one_of_inputs("kwargs")
root_id = kwargs.get("root_id")
- time.sleep(3600)
+ time.sleep(60 * 5)
# 测试报错
if kwargs.get("is_error"):
diff --git a/dbm-ui/backend/ticket/exceptions.py b/dbm-ui/backend/ticket/exceptions.py
index db5482a9d5..387118d25a 100644
--- a/dbm-ui/backend/ticket/exceptions.py
+++ b/dbm-ui/backend/ticket/exceptions.py
@@ -60,6 +60,12 @@ class TodoWrongOperatorException(TicketBaseException):
MESSAGE_TPL = _("错误的todo处理人{username}")
+class TodoDuplicateProcessException(TicketBaseException):
+ ERROR_CODE = "010"
+ MESSAGE = _("重复操作")
+ MESSAGE_TPL = _("重复操作")
+
+
class ApprovalWrongOperatorException(TicketBaseException):
ERROR_CODE = "008"
MESSAGE = _("审批处理异常")
@@ -67,6 +73,6 @@ class ApprovalWrongOperatorException(TicketBaseException):
class TicketFlowsConfigException(TicketBaseException):
- ERROR_CODE = "008"
+ ERROR_CODE = "009"
MESSAGE = _("单据流程设置失败")
MESSAGE_TPL = _("单据流程{ticket_type}设置失败")
diff --git a/dbm-ui/backend/ticket/flow_manager/manager.py b/dbm-ui/backend/ticket/flow_manager/manager.py
index 896ef84f50..8b99309774 100644
--- a/dbm-ui/backend/ticket/flow_manager/manager.py
+++ b/dbm-ui/backend/ticket/flow_manager/manager.py
@@ -10,6 +10,8 @@
"""
import logging
+from django.db import transaction
+
from backend import env
from backend.core import notify
from backend.ticket import constants
@@ -115,15 +117,20 @@ def update_ticket_status(self):
# 其他场景下状态未变更,无需更新DB
return
- if self.ticket.status != target_status:
- origin_status = self.ticket.status
- self.ticket.status = target_status
- self.ticket.save(update_fields=["status", "update_at"])
+ # 原子更新单据状态
+ with transaction.atomic():
+ ticket = Ticket.objects.select_for_update().get(id=self.ticket.id)
+ if ticket.status == target_status:
+ return
+ origin_status, ticket.status = ticket.status, target_status
+ ticket.save(update_fields=["status", "update_at"])
self.ticket_status_trigger(origin_status, target_status)
def ticket_status_trigger(self, origin_status, target_status):
"""单据状态更新后的钩子函数"""
- # 单据状态变更后,发送通知。忽略running
- if target_status != TicketStatus.RUNNING:
+ # 单据状态变更后,发送通知。
+ # 忽略运行中:流转到内置任务无需通知,待继续在todo创建时才触发通知
+ # 忽略待补货:到资源申请节点,单据状态总会流转为待补货,但是只有待补货todo创建才触发通知
+ if target_status not in [TicketStatus.RUNNING, TicketStatus.RESOURCE_REPLENISH]:
notify.send_msg.apply_async(args=(self.ticket.id,))
diff --git a/dbm-ui/backend/ticket/flow_manager/resource.py b/dbm-ui/backend/ticket/flow_manager/resource.py
index 4fa5166810..e1f056e805 100644
--- a/dbm-ui/backend/ticket/flow_manager/resource.py
+++ b/dbm-ui/backend/ticket/flow_manager/resource.py
@@ -21,6 +21,7 @@
from backend.components.dbresource.client import DBResourceApi
from backend.configuration.constants import AffinityEnum
from backend.configuration.models import DBAdministrator
+from backend.core import notify
from backend.db_meta.models import Spec
from backend.db_services.dbresource.exceptions import ResourceApplyException, ResourceApplyInsufficientException
from backend.db_services.ipchooser.constants import CommonEnum
@@ -214,6 +215,7 @@ def create_replenish_todo(self):
flow_id=self.flow_obj.id, ticket_id=self.ticket.id, user=self.ticket.creator, administrators=dba
).to_dict(),
)
+ notify.send_msg.apply_async(args=(self.ticket.id,))
def fetch_apply_params(self, ticket_data):
"""
diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py
index d34e5345a8..c389086d1c 100644
--- a/dbm-ui/backend/ticket/handler.py
+++ b/dbm-ui/backend/ticket/handler.py
@@ -231,7 +231,13 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs):
act_msg = kwargs.get("action_message") or act_msg_tpl
# 审批单据
- params = {"action_message": act_msg}
+ params = {
+ "sn": sn,
+ "action_message": act_msg,
+ "action_type": action,
+ "operator": operator,
+ "bk_username": operator,
+ }
if action == OperateNodeActionType.TRANSITION:
is_approved = kwargs["is_approved"]
itsm_fields = cls.get_itsm_fields(flow.ticket.ticket_type)
@@ -239,11 +245,10 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs):
{"key": itsm_fields[0], "value": json.dumps(is_approved)},
{"key": itsm_fields[1], "value": act_msg},
]
- params.update(sn=sn, state_id=state_id, action_type=action, operator=operator, fields=fields)
+ params.update(state_id=state_id, fields=fields)
ItsmApi.operate_node(params)
# 终止/撤销单据
elif action in [OperateNodeActionType.TERMINATE, OperateNodeActionType.WITHDRAW]:
- params.update(sn=sn, action_type=action, operator=operator)
ItsmApi.operate_ticket(params)
return sn
diff --git a/dbm-ui/backend/ticket/todos/__init__.py b/dbm-ui/backend/ticket/todos/__init__.py
index ad71f9d014..3cf9742f88 100644
--- a/dbm-ui/backend/ticket/todos/__init__.py
+++ b/dbm-ui/backend/ticket/todos/__init__.py
@@ -19,7 +19,7 @@
from backend.constants import DEFAULT_SYSTEM_USER
from backend.ticket.constants import TODO_RUNNING_STATUS
-from backend.ticket.exceptions import TodoWrongOperatorException
+from backend.ticket.exceptions import TodoDuplicateProcessException, TodoWrongOperatorException
from backend.ticket.models import Todo
from blue_krill.data_types.enum import EnumField, StructuredEnum
@@ -55,7 +55,7 @@ def allow_superuser_process(self):
def process(self, username, action, params):
# 当状态已经被确认,则不允许重复操作
if self.todo.status not in TODO_RUNNING_STATUS:
- raise TodoWrongOperatorException(_("当前代办操作已经处理,不能重复处理!"))
+ raise TodoDuplicateProcessException(_("当前代办操作已经处理,不能重复处理!"))
# 允许系统内置用户确认
if username == DEFAULT_SYSTEM_USER:
diff --git a/dbm-ui/backend/ticket/todos/pipeline_todo.py b/dbm-ui/backend/ticket/todos/pipeline_todo.py
index b20b574442..ace2ad5d0b 100644
--- a/dbm-ui/backend/ticket/todos/pipeline_todo.py
+++ b/dbm-ui/backend/ticket/todos/pipeline_todo.py
@@ -74,7 +74,7 @@ def create(cls, ticket, flow, root_id, node_id):
# 当前不存在待确认的todo,则发送通知
if not flow.todo_of_flow.filter(type=TodoType.INNER_APPROVE).count():
- notify.send_msg(ticket.id, flow.id)
+ notify.send_msg.apply_async(args=(ticket.id,))
Todo.objects.create(
name=_("【{}】流程待确认,是否继续?").format(ticket.get_ticket_type_display()),
diff --git a/dbm-ui/backend/utils/time.py b/dbm-ui/backend/utils/time.py
index 90f809bb1d..b5510d6e8a 100644
--- a/dbm-ui/backend/utils/time.py
+++ b/dbm-ui/backend/utils/time.py
@@ -43,7 +43,7 @@ def timezone2timestamp(date: Union[str, datetime.datetime]) -> int:
return int(time_parse(date).timestamp())
-def datetime2str(o_datetime: datetime.datetime, fmt: str = DATETIME_PATTERN, aware_check: bool = True) -> str:
+def datetime2str(o_datetime: datetime.datetime, aware_check: bool = True) -> str:
"""
将时间对象转换为时间字符串,可选时区强校验
"""