Skip to content

Commit

Permalink
support hook participant type
Browse files Browse the repository at this point in the history
  • Loading branch information
blackholll committed Aug 25, 2019
1 parent 88114c8 commit e68b64b
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 18 deletions.
3 changes: 2 additions & 1 deletion apps/ticket/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import path
from apps.ticket.views import TicketListView, TicketView, TicketTransition, TicketFlowlog, TicketFlowStep, TicketState, \
TicketsStates, TicketAccept, TicketDeliver, TicketAddNode, \
TicketAddNodeEnd, TicketField, TicketScriptRetry, TicketComment
TicketAddNodeEnd, TicketField, TicketScriptRetry, TicketComment, TicketHookCallBack

urlpatterns = [
path('', TicketListView.as_view()),
Expand All @@ -17,5 +17,6 @@
path('/<int:ticket_id>/add_node_end', TicketAddNodeEnd.as_view()),
path('/<int:ticket_id>/retry_script', TicketScriptRetry.as_view()),
path('/<int:ticket_id>/comments', TicketComment.as_view()),
path('/<int:ticket_id>/hook_call_back', TicketHookCallBack.as_view()),
path('/states', TicketsStates.as_view()), # 批量获取工单状态
]
29 changes: 26 additions & 3 deletions apps/ticket/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,15 +446,14 @@ def patch(self, request, *args, **kwargs):
class TicketScriptRetry(View):
def post(self, request, *args, **kwargs):
"""
重新执行工单脚本(用于脚本执行出错的情况)
重新执行工单脚本(用于脚本执行出错的情况), 也可用于hook执行失败的情况
:return:
"""
json_str = request.body.decode('utf-8')
if not json_str:
return api_response(-1, 'post参数为空', {})
request_data_dict = json.loads(json_str)
ticket_id = kwargs.get('ticket_id')
# username = request_data_dict.get('username', '')
username = request.META.get('HTTP_USERNAME')

from service.account.account_base_service import AccountBaseService
Expand Down Expand Up @@ -487,7 +486,6 @@ def post(self, request, *args, **kwargs):
return api_response(-1, 'post参数为空', {})
request_data_dict = json.loads(json_str)
ticket_id = kwargs.get('ticket_id')
# username = request_data_dict.get('username', '')
username = request.META.get('HTTP_USERNAME')
suggestion = request_data_dict.get('suggestion', '')
result, msg = TicketBaseService.add_comment(ticket_id, username, suggestion)
Expand All @@ -496,3 +494,28 @@ def post(self, request, *args, **kwargs):
else:
code, msg, data = -1, msg, ''
return api_response(code, msg, data)


class TicketHookCallBack(View):
def post(self, request, *args, **kwargs):
"""
工单hook回调,用于hoot请求后,被请求方执行完任务后回调loonflow,以触发工单继续流转
:param request:
:param args:
:param kwargs:
:return:
"""
ticket_id = kwargs.get('ticket_id')
json_str = request.body.decode('utf-8')
if not json_str:
return api_response(-1, 'post参数为空', {})
request_data_dict = json.loads(json_str)
# {"result":true, "msg":"", field_value:{"xx":1,"bb":2}}
app_name = request.META.get('HTTP_APPNAME')

result, msg = TicketBaseService().hook_call_back(ticket_id, app_name, request_data_dict)
if result:
code, msg, data = 0, 'add ticket comment successful', ''
else:
code, msg, data = -1, msg, ''
return api_response(code, msg, data)
14 changes: 14 additions & 0 deletions service/common/common_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ def gen_signature(cls, app_name):
tar_str = hashlib.md5(ori_str.encode(encoding='utf-8')).hexdigest()
return True, dict(signature=tar_str, timestamp=timestamp)

@classmethod
@auto_log
def gen_hook_signature(cls, token):
"""
生成hook签名
:param token:
:return:
"""
timestamp = str(int(time.time()))
ori_str = timestamp + token
tar_str = hashlib.md5(ori_str.encode(encoding='utf-8')).hexdigest()
return True, dict(signature=tar_str, timestamp=timestamp)


@classmethod
@auto_log
def get_model_field(cls, app_name, model_name):
Expand Down
1 change: 1 addition & 0 deletions service/common/constant_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def __init__(self):
self.PARTICIPANT_TYPE_FIELD = 7 # 工单字段(用户名类型的)
self.PARTICIPANT_TYPE_PARENT_FIELD = 8 # 父工单字段(用户名类型的)
self.PARTICIPANT_TYPE_MULTI_ALL = 9 # 多人全部处理(处理人为多个,且每个人都需要处理),当状态处理人配置为全部处理,且处理人数大于1时,实际的处理人类型则为此
self.PARTICIPANT_TYPE_HOOK = 10 # hook方式,当工单状态叨叨处理人类型配置为kook的状态时,loonflow将触发一个hook请求,被请求方可以执行有些自动化操作然后回调loonflow,

self.TRANSITION_TYPE_COMMON = 1 # 常规流转
self.TRANSITION_TYPE_TIMER = 2 # 定时器流转
Expand Down
95 changes: 84 additions & 11 deletions service/ticket/ticket_base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,14 @@ def new_ticket(cls, request_data_dict, app_name=''):
from tasks import run_flow_task # 放在文件开头会存在循环引用
run_flow_task.apply_async(args=[new_ticket_obj.id, destination_participant, destination_state_id], queue='loonflow')

# 如果下个状态是hook,开始触发hook
if destination_participant_type_id == CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
# 因为工单基础表中不保存hook配置,所以从状态表中获取
state_obj, msg = WorkflowStateService.get_workflow_state_by_id(new_ticket_obj.state_id)
from tasks import flow_hook_task # 放在文件开头会存在循环引用
flow_hook_task.apply_async(args=[new_ticket_obj.id], queue='loonflow')


# 定时器处理逻辑
cls.handle_timer_transition(new_ticket_obj.id, destination_state_id)

Expand Down Expand Up @@ -837,13 +845,14 @@ def get_ticket_format_participant_info(cls, ticket_id):

@classmethod
@auto_log
def ticket_handle_permission_check(cls, ticket_id, username, by_timer=False, by_task=False):
def ticket_handle_permission_check(cls, ticket_id, username, by_timer=False, by_task=False, by_hook=False):
"""
处理权限校验: 获取当前状态是否需要处理, 该用户是否有权限处理
:param ticket_id:
:param username:
:param by_timer:是否为定时器流转
:param by_task:是否为通过脚本流转
:param by_hook:是否hook回调触发的流转
:return:
"""
ticket_obj = TicketRecord.objects.filter(id=ticket_id, is_deleted=0).first()
Expand All @@ -862,6 +871,9 @@ def ticket_handle_permission_check(cls, ticket_id, username, by_timer=False, by_
if by_task and username == 'loonrobot':
# 脚本流转,有权限
return True, dict(need_accept=False, in_add_node=False, msg='脚本流转,放开处理权限')
if by_hook and username == 'loonrobot':
# hook触发流转,有权限
return True, dict(need_accept=False, in_add_node=False, msg='hook触发流转,放开处理权限')

participant_type_id = ticket_obj.participant_type_id
participant = ticket_obj.participant
Expand Down Expand Up @@ -962,14 +974,15 @@ def get_ticket_transition(cls, ticket_id, username):

@classmethod
@auto_log
def handle_ticket(cls, ticket_id, request_data_dict, by_timer=False, by_task=False):
def handle_ticket(cls, ticket_id, request_data_dict, by_timer=False, by_task=False, by_hook=False):
"""
处理工单:校验必填参数,获取当前状态必填字段,更新工单基础字段,更新工单自定义字段, 更新工单流转记录,执行必要的脚本,通知消息
此处逻辑和新建工单有较多重复,下个版本会拆出来
:param ticket_id:
:param request_data_dict:
:param by_timer: 是否通过定时器触发的流转
:param by_task: 是否通过脚本执行完成后触发的流转
:param by_hook: 是否hook回调用触发流转
:return:
"""
transition_id = request_data_dict.get('transition_id', '')
Expand All @@ -984,7 +997,7 @@ def handle_ticket(cls, ticket_id, request_data_dict, by_timer=False, by_task=Fal
return False, '工单不存在或已被删除'

# 判断用户是否有权限处理该工单
has_permission, msg = cls.ticket_handle_permission_check(ticket_id, username, by_timer, by_task)
has_permission, msg = cls.ticket_handle_permission_check(ticket_id, username, by_timer, by_task, by_hook)
if not has_permission:
return False, msg
if msg['need_accept']:
Expand Down Expand Up @@ -1126,6 +1139,11 @@ def handle_ticket(cls, ticket_id, request_data_dict, by_timer=False, by_task=Fal
from tasks import run_flow_task # 放在文件开头会存在循环引用
run_flow_task.apply_async(args=[ticket_id, destination_participant, destination_state_id], queue='loonflow')

# 如果下个状态是hook,开始触发hook
if destination_participant_type_id == CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
from tasks import flow_hook_task # 放在文件开头会存在循环引用
flow_hook_task.apply_async(args=[ticket_id], queue='loonflow')

return True, ''

@classmethod
Expand Down Expand Up @@ -1565,22 +1583,31 @@ def get_ticket_all_field_value(cls, ticket_id):
@auto_log
def retry_ticket_script(cls, ticket_id, username):
"""
重新执行工单脚本
重新执行工单脚本,或重新触发hook
:param ticket_id:
:return:
"""
# 判断工单表记录中最后一次脚本是否执行失败了,即script_run_last_result的值
ticket_obj = TicketRecord.objects.filter(id=ticket_id, is_deleted=0).first()
if not ticket_obj:
return False, 'Ticket is not existed or has been deleted'
if ticket_obj.participant_type_id is not CONSTANT_SERVICE.PARTICIPANT_TYPE_ROBOT:
return False, "The ticket's participant_type is not robot, do not allow retry"
# 先重置上次执行结果
ticket_obj.script_run_last_result = True
ticket_obj.save()
# if ticket_obj.participant_type_id is not CONSTANT_SERVICE.PARTICIPANT_TYPE_ROBOT:
# return False, "The ticket's participant_type is not robot, do not allow retry"

if ticket_obj.participant_type_id == CONSTANT_SERVICE.PARTICIPANT_TYPE_ROBOT:
# 先重置上次执行结果
ticket_obj.script_run_last_result = True
ticket_obj.save()
from tasks import run_flow_task # 放在文件开头会存在循环引用问题
run_flow_task.apply_async(args=[ticket_id, ticket_obj.participant, ticket_obj.state_id, '{}_retry'.format(username)], queue='loonflow')
elif ticket_obj.participant_type_id == CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
ticket_obj.script_run_last_result = True
ticket_obj.save()
from tasks import flow_hook_task
flow_hook_task.apply_async(args=[ticket_id], queue='loonflow')
else:
return False, "The ticket's participant_type is not robot or hook, do not allow retry"

from tasks import run_flow_task # 放在文件开头会存在循环引用问题
run_flow_task.apply_async(args=[ticket_id, ticket_obj.participant, ticket_obj.state_id, '{}_retry'.format(username)], queue='loonflow')

@classmethod
@auto_log
Expand Down Expand Up @@ -1685,6 +1712,10 @@ def get_ticket_state_participant_info(cls, state_id, ticket_id=0, ticket_req_dic
if len(approver.split(',')) > 1:
destination_participant_type_id = CONSTANT_SERVICE.PARTICIPANT_TYPE_MULTI
destination_participant = approver

elif participant_type_id == CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
destination_participant = '***' # 敏感数据,不保存工单基础表中

if destination_participant_type_id in (CONSTANT_SERVICE.PARTICIPANT_TYPE_MULTI, CONSTANT_SERVICE.PARTICIPANT_TYPE_DEPT, CONSTANT_SERVICE.PARTICIPANT_TYPE_ROLE) \
and state_obj.distribute_type_id in (CONSTANT_SERVICE.STATE_DISTRIBUTE_TYPE_RANDOM, CONSTANT_SERVICE.STATE_DISTRIBUTE_TYPE_ALL):
# 处理人为角色,部门,或者角色都可能是为多个人,需要根据状态的分配方式计算实际的处理人
Expand Down Expand Up @@ -1821,3 +1852,45 @@ def add_comment(cls, ticket_id=0, username='', suggestion=''):
return False, msg
return True, ''

@classmethod
@auto_log
def hook_call_back(cls, ticket_id, app_name, request_data_dict):
"""
hook回调
:param ticket_id:
:param app_name:
:param request_data_dict:
:return:
"""
# 校验请求app_name是否有hook回调该工单权限
flag, msg = AccountBaseService().app_ticket_permission_check(app_name, ticket_id)
if not flag:
return False, msg
ticket_obj = TicketRecord.objects.filter(id=ticket_id, is_deleted=0).first()

# 检查工单处理人类型为hook中
if ticket_obj.participant_type_id != CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
return False, '工单当前处理人类型非hook,不执行回调操作'

result = request_data_dict.get('result', True)
msg = request_data_dict.get('msg', '')
field_value = request_data_dict.get('field_value', {}) # 用于更新字段

if result is False:
# hook执行失败了,记录失败状态.以便允许下次再执行
cls.update_ticket_field_value({'script_run_last_result': False})
return True, ''

state_id = ticket_obj.state_id
transition_queryset, msg = WorkflowTransitionService().get_state_transition_queryset(state_id)
transition_id = transition_queryset[0] # hook状态只支持一个流转

new_request_dict = field_value

new_request_dict.update({'transition_id': transition_id, 'suggestion': msg, 'username': 'loonrobot'})

# 执行流转
flag, msg = cls.handle_ticket(ticket_id, new_request_dict, by_timer=False, by_task=False)
if not flag:
return False, msg
return True, ''
80 changes: 79 additions & 1 deletion tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()


import json
import requests
from apps.ticket.models import TicketRecord
from apps.workflow.models import Transition, State, WorkflowScript, Workflow, CustomNotice
from service.account.account_base_service import AccountBaseService
from service.common.constant_service import CONSTANT_SERVICE
from service.ticket.ticket_base_service import TicketBaseService
from service.common.common_service import CommonService
from service.workflow.workflow_transition_service import WorkflowTransitionService
from django.conf import settings

try:
Expand Down Expand Up @@ -221,3 +224,78 @@ def send_ticket_notice(ticket_id):
script_result = False
script_result_msg = e.__str__()
return script_result, script_result_msg


@app.task
def flow_hook_task(ticket_id):
"""
hook 任务
:param ticket_id:
:return:
"""
# 查询工单状态
ticket_obj = TicketRecord.objects.filter(id=ticket_id, is_deleted=0).first()
state_id = ticket_obj.state_id
state_obj = State.objects.filter(id=state_id, is_deleted=0).first()

participant_type_id = state_obj.participant_type_id
if participant_type_id != CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK:
return False, ''
hook_config = state_obj.participant
hook_config_dict= json.loads(hook_config)
hook_url = hook_config_dict.get('hook_url')
hook_token = hook_config_dict.get('hook_token')
wait = hook_config_dict.get('wait')

flag, msg = CommonService().gen_hook_signature(hook_token)
if not flag:
return False, msg
r = requests.post(hook_url, headers=msg, timeout=10)
result = r.json()
if result.get('code') == 0:
# 调用成功
if wait:
all_ticket_data, msg = TicketBaseService().get_ticket_all_field_value(ticket_id)
# date等格式需要转换为str
for key, value in all_ticket_data.items():
if type(value) not in [int, str, bool, float]:
all_ticket_data[key] = str(all_ticket_data[key])

all_ticket_data_json = json.dumps(all_ticket_data)
TicketBaseService().add_ticket_flow_log(dict(ticket_id=ticket_id, transition_id=0,
suggestion=result.get('msg'),
participant_type_id=CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK,
participant='hook', state_id=state_id,
ticket_data=all_ticket_data_json,
creator='loonrobot'
))
return True, ''
else:
# 不等待hook目标回调,直接流转
transition_queryset, msg = WorkflowTransitionService().get_state_transition_queryset(state_id)
transition_id = transition_queryset[0] # hook状态只支持一个流转

new_request_dict = {}
new_request_dict.update({'transition_id': transition_id, 'suggestion': msg, 'username': 'loonrobot'})
# 执行流转
flag, msg = TicketBaseService().handle_ticket(ticket_id, new_request_dict, by_hook=True)
if not flag:
return False, msg

else:
TicketBaseService().update_ticket_field_value({'script_run_last_result': False})

all_ticket_data, msg = TicketBaseService().get_ticket_all_field_value(ticket_id)
# date等格式需要转换为str
for key, value in all_ticket_data.items():
if type(value) not in [int, str, bool, float]:
all_ticket_data[key] = str(all_ticket_data[key])

all_ticket_data_json = json.dumps(all_ticket_data)
TicketBaseService().add_ticket_flow_log(dict(ticket_id=ticket_id, transition_id=0,
suggestion=result.get('msg'),
participant_type_id=CONSTANT_SERVICE.PARTICIPANT_TYPE_HOOK,
participant='hook', state_id=state_id, ticket_data=all_ticket_data_json,
creator='loonrobot'
))

Loading

0 comments on commit e68b64b

Please sign in to comment.