Skip to content

Commit

Permalink
feat(backend): bkchat专属process todo TencentBlueKing#8755
Browse files Browse the repository at this point in the history
  • Loading branch information
iSecloud committed Jan 6, 2025
1 parent 8ea720e commit 5f1303d
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 50 deletions.
59 changes: 35 additions & 24 deletions dbm-ui/backend/core/notify/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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": _("使用「蓝鲸审批助手」终止单据")},
},
}
Expand Down Expand Up @@ -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))
# 邮件的换行要用<br>的html
self.content = self.content.replace("\n", "<br>")
self._cmsi_send_msg(MsgType.MAIL, **kwargs)

def send_voice(self):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
# 根据通知类型获取通知类,以及通知所需的上下文
Expand All @@ -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):
# 获取标题,在群机器人通知则加上@人
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
12 changes: 6 additions & 6 deletions dbm-ui/backend/core/notify/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
所属业务: {{biz_name}}
业务: {{biz_name}}
域名: {{cluster_domains}}
备注: {{remark}}
处理人: {{operators}}
当前处理人: {{operators}}
查看详情: {{detail_address}}\
"""
)
Expand All @@ -29,7 +29,7 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
所属业务: {{biz_name}}
业务: {{biz_name}}
域名: {{cluster_domains}}
完成时间: {{update_time}}
查看详情: {{detail_address}}\
Expand All @@ -41,10 +41,10 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
所属业务: {{biz_name}}
业务: {{biz_name}}
域名: {{cluster_domains}}
失败时间: {{update_time}}
处理人: {{operators}}
当前当前处理人: {{operators}}
查看详情: {{detail_address}}\
"""
)
Expand All @@ -54,7 +54,7 @@
"""\
申请人: {{creator}}
申请时间: {{submit_time}}
所属业务: {{biz_name}}
业务: {{biz_name}}
域名: {{cluster_domains}}
终止时间: {{update_time}}
终止原因: {{terminate_reason}}
Expand Down
2 changes: 1 addition & 1 deletion dbm-ui/backend/db_services/plugin/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
specific language governing permissions and limitations under the License.
"""

SWAGGER_TAG = "plugin"
SWAGGER_TAG = "OpenAPI"
11 changes: 10 additions & 1 deletion dbm-ui/backend/db_services/plugin/ticket/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=_("按钮颜色"))
42 changes: 39 additions & 3 deletions dbm-ui/backend/db_services/plugin/ticket/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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"})
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
8 changes: 7 additions & 1 deletion dbm-ui/backend/ticket/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,19 @@ class TodoWrongOperatorException(TicketBaseException):
MESSAGE_TPL = _("错误的todo处理人{username}")


class TodoDuplicateProcessException(TicketBaseException):
ERROR_CODE = "010"
MESSAGE = _("重复操作")
MESSAGE_TPL = _("重复操作")


class ApprovalWrongOperatorException(TicketBaseException):
ERROR_CODE = "008"
MESSAGE = _("审批处理异常")
MESSAGE_TPL = _("审批处理异常{username}")


class TicketFlowsConfigException(TicketBaseException):
ERROR_CODE = "008"
ERROR_CODE = "009"
MESSAGE = _("单据流程设置失败")
MESSAGE_TPL = _("单据流程{ticket_type}设置失败")
19 changes: 13 additions & 6 deletions dbm-ui/backend/ticket/flow_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,))
2 changes: 2 additions & 0 deletions dbm-ui/backend/ticket/flow_manager/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
11 changes: 8 additions & 3 deletions dbm-ui/backend/ticket/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,19 +231,24 @@ 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)
fields = [
{"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
Expand Down
4 changes: 2 additions & 2 deletions dbm-ui/backend/ticket/todos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 5f1303d

Please sign in to comment.