Skip to content

Commit 5f1303d

Browse files
committed
feat(backend): bkchat专属process todo #8755
1 parent 8ea720e commit 5f1303d

File tree

14 files changed

+127
-50
lines changed

14 files changed

+127
-50
lines changed

dbm-ui/backend/core/notify/handlers.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
from backend.core.notify.exceptions import NotifyBaseException
2525
from backend.core.notify.template import FAILED_TEMPLATE, FINISHED_TEMPLATE, TERMINATE_TEMPLATE, TODO_TEMPLATE
2626
from backend.db_meta.models import AppCache
27+
from backend.exceptions import ApiResultError
2728
from backend.ticket.builders import BuilderFactory
28-
from backend.ticket.constants import TicketStatus, TicketType
29+
from backend.ticket.constants import TicketStatus, TicketType, TodoStatus
2930
from backend.ticket.models import Flow, Ticket
3031
from backend.ticket.todos import ActionType
3132
from backend.utils.cache import func_cache_decorator
32-
from backend.utils.time import datetime2str
3333

3434
logger = logging.getLogger("root")
3535

@@ -78,20 +78,25 @@ def get_actions(msg_type, ticket):
7878
"""获取bkchat操作按钮"""
7979
if ticket.status not in [TicketStatus.APPROVE, TicketStatus.TODO]:
8080
return []
81+
82+
todo = ticket.todo_of_ticket.filter(status=TodoStatus.TODO).first()
83+
if not todo:
84+
return []
85+
8186
# 增加回调按钮,执行和终止
8287
agree_action = {
8388
"name": _("同意") if ticket.status == TicketStatus.APPROVE else _("确认执行"),
8489
"color": "green",
85-
"callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/",
86-
"callback_data": {"action": ActionType.APPROVE.value, "ticket_ids": [ticket.id]},
90+
"callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/",
91+
"callback_data": {"action": ActionType.APPROVE.value, "todo_id": todo.id, "params": {}},
8792
}
8893
refuse_action = {
8994
"name": _("拒绝") if ticket.status == TicketStatus.APPROVE else _("终止单据"),
9095
"color": "red",
91-
"callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/",
96+
"callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/",
9297
"callback_data": {
9398
"action": ActionType.TERMINATE.value,
94-
"ticket_ids": [ticket.id],
99+
"todo_id": todo.id,
95100
"params": {"remark": _("使用「蓝鲸审批助手」终止单据")},
96101
},
97102
}
@@ -178,6 +183,8 @@ def send_mail(self, sender: str = None, cc: list = None):
178183
kwargs.update(sender=sender)
179184
if cc:
180185
kwargs.update(cc__username=",".join(cc))
186+
# 邮件的换行要用<br>的html
187+
self.content = self.content.replace("\n", "<br>")
181188
self._cmsi_send_msg(MsgType.MAIL, **kwargs)
182189

183190
def send_voice(self):
@@ -193,7 +200,9 @@ def send_rtx(self):
193200
self._cmsi_send_msg(MsgType.RTX.value)
194201

195202
def send_sms(self):
196-
"""发送企微消息"""
203+
"""发送短信消息"""
204+
# 短信消息没有标题参数,直接把标题和内容放在一起
205+
self.content = f"{self.title}\n{self.content}"
197206
self._cmsi_send_msg(MsgType.SMS.value)
198207

199208
def send_wecom_robot(self):
@@ -238,7 +247,11 @@ def __init__(self, ticket_id: int, flow_id: int = None):
238247
def get_support_msg_types(cls):
239248
# 获取当前环境下支持的通知类型
240249
# 所有的拓展方式都需要接入CMSI,所以直接返回CMSI支持方式即可
241-
return CmsiApi.get_msg_type()
250+
# 暂不暴露微信的通知方式
251+
msg_types = CmsiApi.get_msg_type()
252+
msg_type_map = {msg["type"]: msg for msg in msg_types}
253+
msg_type_map[MsgType.WEIXIN.value]["is_active"] = False
254+
return list(msg_type_map.values())
242255

243256
def get_notify_class(self, msg_type: str):
244257
# 根据通知类型获取通知类,以及通知所需的上下文
@@ -253,15 +266,17 @@ def get_receivers(self):
253266
biz_helpers = BizSettings.get_assistance(self.bk_biz_id)
254267
creator = [self.ticket.creator]
255268
# 待审批:审批人
256-
# 待执行、待补货、待确认、已失败、已完成、已终止: 提单人、协助人
269+
# 待执行、待补货、待确认、已失败、已完成、已终止:提单人、协助人
257270
# 暂不通知DBA
258271
if self.phase in [TicketStatus.PENDING]:
259-
return creator
272+
receivers = creator
260273
elif self.phase in [TicketStatus.APPROVE]:
261274
itsm_builder = BuilderFactory.get_builder_cls(self.ticket.ticket_type).itsm_flow_builder(self.ticket)
262-
return itsm_builder.get_approvers().split(",")
275+
receivers = itsm_builder.get_approvers().split(",")
263276
else:
264-
return creator + biz_helpers
277+
receivers = creator + biz_helpers
278+
# 去重后返回
279+
return list(dict.fromkeys(receivers))
265280

266281
def render_msg_template(self, msg_type: str):
267282
# 获取标题,在群机器人通知则加上@人
@@ -289,8 +304,8 @@ def render_msg_template(self, msg_type: str):
289304
"cluster_domains": ",".join(self.clusters),
290305
"remark": self.ticket.remark,
291306
"creator": self.ticket.creator,
292-
"submit_time": datetime2str(self.ticket.create_at),
293-
"update_time": datetime2str(self.ticket.update_at),
307+
"submit_time": self.ticket.create_at.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"),
308+
"update_time": self.ticket.update_at.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"),
294309
"status": TicketStatus.get_choice_label(self.phase),
295310
"operators": ",".join(self.ticket.get_current_operators()),
296311
"detail_address": self.ticket.url,
@@ -325,17 +340,13 @@ def send_msg(self):
325340
if msg_type == MsgType.WECOM_ROBOT:
326341
self.receivers = send_msg_config.get(MsgType.WECOM_ROBOT.value, [])
327342

328-
notify_class(title, content, self.receivers).send_msg(msg_type, context=context)
343+
try:
344+
notify_class(title, content, self.receivers).send_msg(msg_type, context=context)
345+
except (ApiResultError, Exception) as e:
346+
logger.error(_("[{}]消息发送失败,错误信息: {}").format(MsgType.get_choice_label(msg_type), e))
329347

330348

331349
@shared_task
332-
def send_msg(ticket_id: int, flow_id: int = None, raise_exception: bool = False):
350+
def send_msg(ticket_id: int, flow_id: int = None):
333351
# 可异步发送消息,非阻塞路径默认不抛出异常
334-
try:
335-
NotifyAdapter(ticket_id, flow_id).send_msg()
336-
except Exception as e:
337-
err_msg = _("消息发送失败,错误信息:{}").format(e)
338-
if not raise_exception:
339-
logger.error(err_msg)
340-
else:
341-
raise NotifyBaseException(err_msg)
352+
NotifyAdapter(ticket_id, flow_id).send_msg()

dbm-ui/backend/core/notify/template.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
"""\
1717
申请人: {{creator}}
1818
申请时间: {{submit_time}}
19-
所属业务: {{biz_name}}
19+
业务: {{biz_name}}
2020
域名: {{cluster_domains}}
2121
备注: {{remark}}
22-
处理人: {{operators}}
22+
当前处理人: {{operators}}
2323
查看详情: {{detail_address}}\
2424
"""
2525
)
@@ -29,7 +29,7 @@
2929
"""\
3030
申请人: {{creator}}
3131
申请时间: {{submit_time}}
32-
所属业务: {{biz_name}}
32+
业务: {{biz_name}}
3333
域名: {{cluster_domains}}
3434
完成时间: {{update_time}}
3535
查看详情: {{detail_address}}\
@@ -41,10 +41,10 @@
4141
"""\
4242
申请人: {{creator}}
4343
申请时间: {{submit_time}}
44-
所属业务: {{biz_name}}
44+
业务: {{biz_name}}
4545
域名: {{cluster_domains}}
4646
失败时间: {{update_time}}
47-
处理人: {{operators}}
47+
当前当前处理人: {{operators}}
4848
查看详情: {{detail_address}}\
4949
"""
5050
)
@@ -54,7 +54,7 @@
5454
"""\
5555
申请人: {{creator}}
5656
申请时间: {{submit_time}}
57-
所属业务: {{biz_name}}
57+
业务: {{biz_name}}
5858
域名: {{cluster_domains}}
5959
终止时间: {{update_time}}
6060
终止原因: {{terminate_reason}}

dbm-ui/backend/db_services/plugin/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
specific language governing permissions and limitations under the License.
1010
"""
1111

12-
SWAGGER_TAG = "plugin"
12+
SWAGGER_TAG = "OpenAPI"

dbm-ui/backend/db_services/plugin/ticket/serializers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@
1212
from django.utils.translation import gettext_lazy as _
1313
from rest_framework import serializers
1414

15-
from backend.ticket.serializers import BatchTicketOperateSerializer
15+
from backend.ticket.serializers import BatchTicketOperateSerializer, TodoOperateSerializer
1616

1717

1818
class OpenAPIBatchTicketOperateSerializer(BatchTicketOperateSerializer):
1919
username = serializers.CharField(help_text=_("操作者"))
20+
21+
22+
class OpenAPIBkChatProcessTodoSerializer(TodoOperateSerializer):
23+
username = serializers.CharField(help_text=_("操作者"))
24+
25+
26+
class OpenAPIBkChatProcessTodoResponseSerializer(serializers.Serializer):
27+
response_msg = serializers.CharField(help_text=_("返回信息"))
28+
response_color = serializers.CharField(help_text=_("按钮颜色"))

dbm-ui/backend/db_services/plugin/ticket/views.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@
1717
from rest_framework.response import Response
1818

1919
from backend.db_services.plugin.constants import SWAGGER_TAG
20-
from backend.db_services.plugin.ticket.serializers import OpenAPIBatchTicketOperateSerializer
20+
from backend.db_services.plugin.ticket.serializers import (
21+
OpenAPIBatchTicketOperateSerializer,
22+
OpenAPIBkChatProcessTodoResponseSerializer,
23+
OpenAPIBkChatProcessTodoSerializer,
24+
)
2125
from backend.db_services.plugin.view import BaseOpenAPIViewSet
26+
from backend.ticket.constants import TodoStatus, TodoType
27+
from backend.ticket.exceptions import TodoDuplicateProcessException
2228
from backend.ticket.handler import TicketHandler
23-
from backend.ticket.serializers import TodoSerializer
29+
from backend.ticket.models import Todo
30+
from backend.ticket.todos import TodoActorFactory
2431

2532
logger = logging.getLogger("root")
2633

@@ -29,10 +36,39 @@ class TicketViewSet(BaseOpenAPIViewSet):
2936
@swagger_auto_schema(
3037
operation_summary=_("批量单据待办处理"),
3138
request_body=OpenAPIBatchTicketOperateSerializer(),
32-
responses={status.HTTP_200_OK: TodoSerializer(many=True)},
3339
tags=[SWAGGER_TAG],
3440
)
3541
@action(methods=["POST"], detail=False, serializer_class=OpenAPIBatchTicketOperateSerializer)
3642
def batch_process_ticket(self, request, *args, **kwargs):
3743
params = self.params_validate(self.get_serializer_class())
3844
return Response(TicketHandler.batch_process_ticket(**params))
45+
46+
@swagger_auto_schema(
47+
operation_summary=_("待办处理(bkchat专属)"),
48+
request_body=OpenAPIBkChatProcessTodoSerializer(),
49+
responses={status.HTTP_200_OK: OpenAPIBkChatProcessTodoResponseSerializer()},
50+
tags=[SWAGGER_TAG],
51+
)
52+
@action(methods=["POST"], detail=False, serializer_class=OpenAPIBkChatProcessTodoSerializer)
53+
def bkchat_process_todo(self, request, *args, **kwargs):
54+
"""
55+
bkchat专属的待办处理,区别主要是返回结构不同
56+
"""
57+
params = self.params_validate(self.get_serializer_class())
58+
59+
todo = Todo.objects.get(id=params["todo_id"])
60+
if todo.type not in [TodoType.ITSM, TodoType.APPROVE]:
61+
return Response({"response_msg": _("暂不支持该类型{}todo的处理").fromat(todo.type), "response_color": "red"})
62+
63+
# 确认todo,忽略重复操作
64+
try:
65+
TodoActorFactory.actor(todo).process(params["username"], params["action"], params["params"])
66+
except TodoDuplicateProcessException:
67+
pass
68+
69+
# 根据操作类型获取文案和按钮颜色
70+
todo.refresh_from_db()
71+
if todo.status == TodoStatus.DONE_FAILED:
72+
return Response({"response_msg": _("{} 已终止").format(todo.done_by), "response_color": "red"})
73+
elif todo.status == TodoStatus.DONE_SUCCESS:
74+
return Response({"response_msg": _("{} 已确认").format(todo.done_by), "response_color": "green"})

dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def _execute(self, data, parent_data, callback=None) -> bool:
2525
kwargs = data.get_one_of_inputs("kwargs")
2626
root_id = kwargs.get("root_id")
2727

28-
time.sleep(3600)
28+
time.sleep(60 * 5)
2929

3030
# 测试报错
3131
if kwargs.get("is_error"):

dbm-ui/backend/ticket/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,19 @@ class TodoWrongOperatorException(TicketBaseException):
6060
MESSAGE_TPL = _("错误的todo处理人{username}")
6161

6262

63+
class TodoDuplicateProcessException(TicketBaseException):
64+
ERROR_CODE = "010"
65+
MESSAGE = _("重复操作")
66+
MESSAGE_TPL = _("重复操作")
67+
68+
6369
class ApprovalWrongOperatorException(TicketBaseException):
6470
ERROR_CODE = "008"
6571
MESSAGE = _("审批处理异常")
6672
MESSAGE_TPL = _("审批处理异常{username}")
6773

6874

6975
class TicketFlowsConfigException(TicketBaseException):
70-
ERROR_CODE = "008"
76+
ERROR_CODE = "009"
7177
MESSAGE = _("单据流程设置失败")
7278
MESSAGE_TPL = _("单据流程{ticket_type}设置失败")

dbm-ui/backend/ticket/flow_manager/manager.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"""
1111
import logging
1212

13+
from django.db import transaction
14+
1315
from backend import env
1416
from backend.core import notify
1517
from backend.ticket import constants
@@ -115,15 +117,20 @@ def update_ticket_status(self):
115117
# 其他场景下状态未变更,无需更新DB
116118
return
117119

118-
if self.ticket.status != target_status:
119-
origin_status = self.ticket.status
120-
self.ticket.status = target_status
121-
self.ticket.save(update_fields=["status", "update_at"])
120+
# 原子更新单据状态
121+
with transaction.atomic():
122+
ticket = Ticket.objects.select_for_update().get(id=self.ticket.id)
123+
if ticket.status == target_status:
124+
return
125+
origin_status, ticket.status = ticket.status, target_status
126+
ticket.save(update_fields=["status", "update_at"])
122127
self.ticket_status_trigger(origin_status, target_status)
123128

124129
def ticket_status_trigger(self, origin_status, target_status):
125130
"""单据状态更新后的钩子函数"""
126131

127-
# 单据状态变更后,发送通知。忽略running
128-
if target_status != TicketStatus.RUNNING:
132+
# 单据状态变更后,发送通知。
133+
# 忽略运行中:流转到内置任务无需通知,待继续在todo创建时才触发通知
134+
# 忽略待补货:到资源申请节点,单据状态总会流转为待补货,但是只有待补货todo创建才触发通知
135+
if target_status not in [TicketStatus.RUNNING, TicketStatus.RESOURCE_REPLENISH]:
129136
notify.send_msg.apply_async(args=(self.ticket.id,))

dbm-ui/backend/ticket/flow_manager/resource.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from backend.components.dbresource.client import DBResourceApi
2222
from backend.configuration.constants import AffinityEnum
2323
from backend.configuration.models import DBAdministrator
24+
from backend.core import notify
2425
from backend.db_meta.models import Spec
2526
from backend.db_services.dbresource.exceptions import ResourceApplyException, ResourceApplyInsufficientException
2627
from backend.db_services.ipchooser.constants import CommonEnum
@@ -214,6 +215,7 @@ def create_replenish_todo(self):
214215
flow_id=self.flow_obj.id, ticket_id=self.ticket.id, user=self.ticket.creator, administrators=dba
215216
).to_dict(),
216217
)
218+
notify.send_msg.apply_async(args=(self.ticket.id,))
217219

218220
def fetch_apply_params(self, ticket_data):
219221
"""

dbm-ui/backend/ticket/handler.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,24 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs):
231231
act_msg = kwargs.get("action_message") or act_msg_tpl
232232

233233
# 审批单据
234-
params = {"action_message": act_msg}
234+
params = {
235+
"sn": sn,
236+
"action_message": act_msg,
237+
"action_type": action,
238+
"operator": operator,
239+
"bk_username": operator,
240+
}
235241
if action == OperateNodeActionType.TRANSITION:
236242
is_approved = kwargs["is_approved"]
237243
itsm_fields = cls.get_itsm_fields(flow.ticket.ticket_type)
238244
fields = [
239245
{"key": itsm_fields[0], "value": json.dumps(is_approved)},
240246
{"key": itsm_fields[1], "value": act_msg},
241247
]
242-
params.update(sn=sn, state_id=state_id, action_type=action, operator=operator, fields=fields)
248+
params.update(state_id=state_id, fields=fields)
243249
ItsmApi.operate_node(params)
244250
# 终止/撤销单据
245251
elif action in [OperateNodeActionType.TERMINATE, OperateNodeActionType.WITHDRAW]:
246-
params.update(sn=sn, action_type=action, operator=operator)
247252
ItsmApi.operate_ticket(params)
248253

249254
return sn

dbm-ui/backend/ticket/todos/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from backend.constants import DEFAULT_SYSTEM_USER
2121
from backend.ticket.constants import TODO_RUNNING_STATUS
22-
from backend.ticket.exceptions import TodoWrongOperatorException
22+
from backend.ticket.exceptions import TodoDuplicateProcessException, TodoWrongOperatorException
2323
from backend.ticket.models import Todo
2424
from blue_krill.data_types.enum import EnumField, StructuredEnum
2525

@@ -55,7 +55,7 @@ def allow_superuser_process(self):
5555
def process(self, username, action, params):
5656
# 当状态已经被确认,则不允许重复操作
5757
if self.todo.status not in TODO_RUNNING_STATUS:
58-
raise TodoWrongOperatorException(_("当前代办操作已经处理,不能重复处理!"))
58+
raise TodoDuplicateProcessException(_("当前代办操作已经处理,不能重复处理!"))
5959

6060
# 允许系统内置用户确认
6161
if username == DEFAULT_SYSTEM_USER:

0 commit comments

Comments
 (0)