提交所有代码到 qiaoxinjiu 分支

This commit is contained in:
qiaoxinjiu
2026-05-11 14:29:16 +08:00
parent 01a4ac8ea1
commit 2fea5adb44
59 changed files with 4957 additions and 1603 deletions

View File

@@ -0,0 +1,53 @@
# encoding: UTF-8
from flask import g
from const import AUTOMATION_CALLBACK_SECRET
from .baseCrudController import BaseCrudController
from ..service.automationService import AutomationService
class AutomationController(BaseCrudController):
def validate_callback_secret(self):
callback_secret = self.req_data.get('_callback_secret')
if AUTOMATION_CALLBACK_SECRET and callback_secret != AUTOMATION_CALLBACK_SECRET:
return False, '回调鉴权失败'
return True, ''
def case_run(self):
return AutomationService.create_case_execution(self.session, self.req_data, getattr(g, 'current_user_id', None))
def plan_run(self):
return AutomationService.create_plan_execution(self.session, self.req_data, getattr(g, 'current_user_id', None))
def execution_list(self):
return AutomationService.list_executions(self.session, self.req_data)
def execution_detail(self):
execution_id = self._get(self.req_data, 'executionId', 'id')
if not execution_id:
return {}, 'executionId 为必传参数'
return AutomationService.get_execution_detail(self.session, execution_id)
def execution_case_list(self):
return AutomationService.list_execution_cases(self.session, self.req_data)
def execution_case_pull(self):
execution_id = self._get(self.req_data, 'executionId', 'execution_id')
if not execution_id:
return {}, 'executionId 为必传参数'
return AutomationService.pull_execution_cases(self.session, execution_id, self.req_data.get('_callback_token'))
def execution_queued(self):
return AutomationService.mark_execution_queued(self.session, self.req_data)
def execution_start(self):
return AutomationService.mark_execution_started(self.session, self.req_data)
def execution_case_result(self):
return AutomationService.save_case_result(self.session, self.req_data)
def execution_finish(self):
return AutomationService.finish_execution(self.session, self.req_data)
def execution_abort(self):
return AutomationService.abort_execution(self.session, self.req_data)

View File

@@ -169,10 +169,12 @@ class CaseController(BaseCrudController):
steps_value = self._get(self.req_data, 'steps', default='')
if isinstance(steps_value, (list, dict)):
steps_value = ''
product_id = self._get(self.req_data, 'productId')
module_id = self._get(self.req_data, 'moduleId')
add_info = {
'project_id': project_id,
'module_id': self._get(self.req_data, 'moduleId'),
'case_key': self._get(self.req_data, 'caseKey') or CaseService.next_case_key(self.session, project_id),
'module_id': module_id,
'case_key': self._get(self.req_data, 'caseKey') or CaseService.next_case_key(self.session, project_id, module_id, product_id),
'title': title,
'preconditions': self._get(self.req_data, 'preconditions'),
'steps': steps_value,
@@ -355,7 +357,7 @@ class CaseController(BaseCrudController):
retry_count = 0
max_retries = 5
case_key = CaseService.next_case_key(self.session, project_id)
case_key = CaseService.next_case_key(self.session, project_id, module_id)
while retry_count < max_retries:
try:

View File

@@ -56,7 +56,7 @@ class PlanController(BaseCrudController):
name = self._get(self.req_data, 'name')
if not project_id or not name:
return 0, 'projectId、name 为必传参数'
add_info = {'project_id': project_id, 'name': name, 'version': self._get(self.req_data, 'version'), 'description': self._get(self.req_data, 'description'), 'start_date': self._get(self.req_data, 'startDate', 'start_time'), 'end_date': self._get(self.req_data, 'endDate', 'end_time'), 'owner_id': self._get(self.req_data, 'ownerId', 'owner_id'), 'status': int(self._get(self.req_data, 'status', default=0)), 'environment_id': self._get(self.req_data, 'environmentId', 'environment_id'), 'is_delete': 0}
add_info = {'project_id': project_id, 'name': name, 'version': self._get(self.req_data, 'version'), 'description': self._get(self.req_data, 'description'), 'start_date': self._get(self.req_data, 'startDate', 'start_time'), 'end_date': self._get(self.req_data, 'endDate', 'end_time'), 'owner_id': self._get(self.req_data, 'ownerId', 'owner_id'), 'status': int(self._get(self.req_data, 'status', default=0)), 'environment_id': self._get(self.req_data, 'environmentId', 'environment_id'), 'jenkins_url': self._get(self.req_data, 'jenkinsUrl', 'jenkins_url'), 'is_auto': int(self._get(self.req_data, 'isAuto', 'is_auto', default=0)), 'is_delete': 0}
return PlanService.create(self.session, TestPlan, add_info)
def plan_update(self):
@@ -65,7 +65,7 @@ class PlanController(BaseCrudController):
if not plan_id:
return 0, 'planId 为必传参数'
update_info = {}
for req_keys, column_key in [(('name', 'name'), 'name'), (('version', 'version'), 'version'), (('description', 'description'), 'description'), (('startDate', 'start_time', 'start_date'), 'start_date'), (('endDate', 'end_time', 'end_date'), 'end_date'), (('ownerId', 'owner_id'), 'owner_id'), (('status', 'status'), 'status'), (('environmentId', 'environment_id'), 'environment_id')]:
for req_keys, column_key in [(('name', 'name'), 'name'), (('version', 'version'), 'version'), (('description', 'description'), 'description'), (('startDate', 'start_time', 'start_date'), 'start_date'), (('endDate', 'end_time', 'end_date'), 'end_date'), (('ownerId', 'owner_id'), 'owner_id'), (('status', 'status'), 'status'), (('environmentId', 'environment_id'), 'environment_id'), (('jenkinsUrl', 'jenkins_url'), 'jenkins_url'), (('isAuto', 'is_auto'), 'is_auto')]:
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
@@ -99,7 +99,7 @@ class PlanController(BaseCrudController):
return PlanService.batch_create(self.session, PlanCase, batch_info_list)
def plan_case_list(self):
plan_id = self._get(self.req_data, 'planId')
plan_id = self._get(self.req_data, 'planId', 'plan_id')
filters = [PlanCase.plan_id == int(plan_id)] if plan_id else []
round_no = self._get(self.req_data, 'roundNo')
if round_no not in (None, ''):
@@ -151,38 +151,9 @@ class PlanController(BaseCrudController):
update_info = {'status': int(self._get(self.req_data, 'status', default=0)), 'actual_result': self._get(self.req_data, 'actualResult'), 'defect_links': self._get(self.req_data, 'defectLinks', default=[]), 'attachments': self._get(self.req_data, 'attachments', default=[]), 'executed_time': datetime.now(), 'execution_duration': self._get(self.req_data, 'executionDuration')}
result = PlanService.update_by_id(self.session, PlanCase, plan_case_id, update_info, soft_delete=False)
self._update_plan_status(plan_id)
PlanService.refresh_plan_status(self.session, plan_id)
return result
def _update_plan_status(self, plan_id):
total = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id).count()
if total == 0:
return
unexecuted_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 0).count()
passed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 1).count()
failed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status.in_([2, 3])).count()
plan = PlanService.get_by_id(self.session, TestPlan, plan_id)
if not plan:
return
if plan.status == 3:
return
if unexecuted_count == 0:
if failed_count == 0:
new_status = 4
else:
new_status = 2
elif unexecuted_count < total:
new_status = 1
else:
new_status = plan.status
if new_status != plan.status:
PlanService.update_by_id(self.session, TestPlan, plan_id, {'status': new_status})
def progress(self):
"""查询计划进度统计。"""

View File

@@ -208,9 +208,6 @@ class RbacController(BaseCrudController):
(('name',), 'name'),
(('code',), 'code'),
(('type',), 'type'),
(('path',), 'path'),
(('component',), 'component'),
(('icon',), 'icon'),
(('permissionCode', 'permission_code'), 'permission_code'),
(('sort',), 'sort'),
(('visible',), 'visible'),
@@ -220,12 +217,30 @@ class RbacController(BaseCrudController):
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
for key in ['path', 'component', 'icon']:
if key in self.req_data:
update_info[key] = self.req_data[key]
return RbacService.update_by_id(self.session, Menu, menu_id, update_info)
def menu_delete(self):
menu_id = self._get(self.req_data, 'menuId', 'id')
if not menu_id:
return 0, 'menuId 为必传参数'
menu = RbacService.get_by_id(self.session, Menu, menu_id)
if menu and menu.permission_code:
permission = self.session.query(Permission).filter(
Permission.code == menu.permission_code,
Permission.is_delete == 0
).first()
if permission:
self.session.query(RolePermission).filter(
RolePermission.permission_id == permission.id,
RolePermission.is_delete == 0
).update({'is_delete': 1})
return RbacService.delete_by_id(self.session, Menu, menu_id)
def role_permission_list(self):
@@ -254,4 +269,44 @@ class RbacController(BaseCrudController):
menu_ids = self._get(self.req_data, 'menuIds', default=[])
if not role_id:
return 0, 'roleId 为必传参数'
if isinstance(menu_ids, str):
import json
menu_ids = json.loads(menu_ids)
if not isinstance(menu_ids, list):
menu_ids = []
menu_permission_codes = RbacService.get_menu_permission_codes(self.session, menu_ids)
permission_ids = RbacService.get_permission_ids_by_codes(self.session, menu_permission_codes)
existing_permission_ids = RbacService.get_role_permission_ids(self.session, role_id)
deleted_permission_ids = [pid for pid in existing_permission_ids if pid not in permission_ids]
if deleted_permission_ids:
self.session.query(RolePermission).filter(
RolePermission.role_id == int(role_id),
RolePermission.permission_id.in_(deleted_permission_ids),
RolePermission.is_delete == 0
).update({'is_delete': 1})
new_permission_ids = [pid for pid in permission_ids if pid not in existing_permission_ids]
if new_permission_ids:
existing_deleted = self.session.query(RolePermission).filter(
RolePermission.role_id == int(role_id),
RolePermission.permission_id.in_(new_permission_ids),
RolePermission.is_delete == 1
).all()
existing_deleted_map = {rp.permission_id: rp for rp in existing_deleted}
for permission_id in new_permission_ids:
if permission_id in existing_deleted_map:
existing_deleted_map[permission_id].is_delete = 0
else:
self.session.add(RolePermission(
role_id=int(role_id),
permission_id=permission_id,
is_delete=0
))
return RbacService.assign_menus(self.session, role_id, menu_ids)

View File

@@ -2,7 +2,7 @@
from .baseCrudController import BaseCrudController
from ..model.userModel import User
from ..service.userService import UserService
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token, create_refresh_token
class UserController(BaseCrudController):
@@ -118,11 +118,14 @@ class UserController(BaseCrudController):
return {}, '用户已禁用!'
UserService.update_last_login_time(self.session, user.id)
token, expire_seconds = create_token(user.id)
refresh_token, refresh_expire_seconds = create_refresh_token(user.id)
ret = self.serialize(user, ['is_delete', 'password_hash'])
ret['role_ids'] = UserService.get_user_role_ids(self.session, user.id)
ret['token'] = token
ret['token_type'] = 'Bearer'
ret['expires_in'] = expire_seconds
ret['refresh_token'] = refresh_token
ret['refresh_expires_in'] = refresh_expire_seconds
ret['refresh_threshold_seconds'] = TOKEN_REFRESH_THRESHOLD_SECONDS
ret['refresh_mechanism'] = '请求任意已登录接口时若token剩余有效期小于阈值则自动续期到完整有效期'
return ret, ''

Binary file not shown.

View File

@@ -0,0 +1,129 @@
# encoding: UTF-8
from sqlalchemy import func
from logger import logger
from ..model.automationModel import AutoExecution, AutoExecutionCase
from ..model.caseModel import TestCase
from ..model.planModel import PlanCase, TestPlan
class AutomationDao(object):
@staticmethod
def create_execution(session, add_info):
obj = AutoExecution(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'AutoExecution新增失败{err}')
return 0, f'新增失败!{err}'
return obj, ''
@staticmethod
def batch_create_execution_cases(session, batch_info_list):
if not batch_info_list:
return [], ''
objs = [AutoExecutionCase(**info) for info in batch_info_list]
session.add_all(objs)
err = session.done(close=False)
if err:
logger.warning(f'AutoExecutionCase批量新增失败{err}')
return [], f'批量新增失败!{err}'
return objs, ''
@staticmethod
def update_execution_by_id(session, execution_id, update_info):
update_res = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'AutoExecution更新失败id: {execution_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应执行记录!'
return int(execution_id), ''
@staticmethod
def get_execution_by_id(session, execution_id):
return session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
@staticmethod
def list_execution_by_filters(session, filters, page=1, limit=20):
query = session.query(AutoExecution).filter(*filters)
total = query.count()
items = query.order_by(AutoExecution.created_time.desc()).offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return items, total
@staticmethod
def get_execution_case_by_id(session, execution_case_id):
return session.query(AutoExecutionCase).filter(AutoExecutionCase.id == int(execution_case_id)).first()
@staticmethod
def get_execution_case_by_unique(session, execution_id, case_id, plan_case_id=None):
filters = [AutoExecutionCase.execution_id == int(execution_id), AutoExecutionCase.case_id == int(case_id)]
if plan_case_id:
filters.append(AutoExecutionCase.plan_case_id == int(plan_case_id))
return session.query(AutoExecutionCase).filter(*filters).order_by(AutoExecutionCase.id.asc()).first()
@staticmethod
def update_execution_case_by_id(session, execution_case_id, update_info):
update_res = session.query(AutoExecutionCase).filter(AutoExecutionCase.id == int(execution_case_id)).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'AutoExecutionCase更新失败id: {execution_case_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应执行明细!'
return int(execution_case_id), ''
@staticmethod
def list_execution_case_by_filters(session, filters, page=1, limit=20):
query = session.query(AutoExecutionCase).filter(*filters)
total = query.count()
items = query.order_by(AutoExecutionCase.id.asc()).offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return items, total
@staticmethod
def count_execution_case_summary(session, execution_id):
rows = session.query(AutoExecutionCase.status, func.count(AutoExecutionCase.id)).filter(
AutoExecutionCase.execution_id == int(execution_id)
).group_by(AutoExecutionCase.status).all()
summary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
for status, count in rows:
summary[int(status)] = int(count)
summary['total'] = sum(summary.values())
return summary
@staticmethod
def query_case_auto_item(session, case_id):
return session.query(TestCase).filter(
TestCase.id == int(case_id), TestCase.is_delete == 0, TestCase.is_auto == 1
).first()
@staticmethod
def query_plan_auto_cases(session, plan_id, round_no=None, case_ids=None):
query = session.query(PlanCase, TestCase).join(
TestCase, PlanCase.case_id == TestCase.id
).filter(
PlanCase.plan_id == int(plan_id),
TestCase.is_delete == 0,
TestCase.is_auto == 1
)
if round_no not in (None, ''):
query = query.filter(PlanCase.round_no == int(round_no))
if case_ids:
query = query.filter(PlanCase.case_id.in_([int(case_id) for case_id in case_ids]))
return query.order_by(PlanCase.id.asc()).all()
@staticmethod
def update_plan_case_result(session, plan_case_id, update_info):
update_res = session.query(PlanCase).filter(PlanCase.id == int(plan_case_id)).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'PlanCase更新失败id: {plan_case_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应计划用例!'
return int(plan_case_id), ''
@staticmethod
def get_plan_by_id(session, plan_id):
return session.query(TestPlan).filter(TestPlan.id == int(plan_id), TestPlan.is_delete == 0).first()

View File

@@ -56,9 +56,166 @@ class CaseDao(object):
return CaseDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def next_case_key(session, project_id):
count_num = session.query(func.count(TestCase.id)).filter(TestCase.project_id == int(project_id)).scalar() or 0
return 'TC-{:03d}'.format(count_num + 1)
def next_case_key(session, project_id, module_id=None, product_id=None):
from ..model.productModel import Product
from ..model.projectModel import Project
from ..model.caseModel import Module
product_abbr = ''
if product_id:
product = session.query(Product).filter(Product.id == int(product_id), Product.is_delete == 0).first()
if product and product.name:
product_abbr = CaseDao._generate_abbreviation(product.name)
project_abbr = ''
project = session.query(Project).filter(Project.id == int(project_id), Project.is_delete == 0).first()
if project and project.name:
project_abbr = CaseDao._generate_abbreviation(project.name)
module_abbr = ''
if module_id:
module = session.query(Module).filter(Module.id == int(module_id), Module.is_delete == 0).first()
if module and module.name:
module_abbr = CaseDao._generate_abbreviation(module.name)
parts = ['TC']
if product_abbr:
parts.append(product_abbr)
if project_abbr:
parts.append(project_abbr)
if module_abbr:
parts.append(module_abbr)
prefix = '-'.join(parts)
count_num = session.query(func.count(TestCase.id)).filter(
TestCase.project_id == int(project_id),
TestCase.is_delete == 0,
TestCase.case_key.like(f'{prefix}-%')
).scalar() or 0
return '{}-{:03d}'.format(prefix, count_num + 1)
@staticmethod
def _generate_abbreviation(name):
import re
chinese_pattern = re.compile(r'[\u4e00-\u9fff]+')
english_pattern = re.compile(r'[a-zA-Z]+')
chinese_chars = chinese_pattern.findall(name)
if chinese_chars:
full_chinese = ''.join(chinese_chars)
abbr = ''.join([CaseDao._get_pinyin_first_char(c) for c in full_chinese[:4]])
abbr = abbr.lower()
if len(abbr) < 2:
abbr = abbr.ljust(2, 'n')
return abbr
english_words = english_pattern.findall(name)
if english_words:
abbr = english_words[0].lower()[:4]
if len(abbr) < 2:
abbr = abbr.ljust(2, 'x')
return abbr
abbr = name.lower()[:4]
if len(abbr) < 2:
abbr = abbr.ljust(2, 'x')
return abbr
@staticmethod
def _get_pinyin_first_char(char):
pinyin_map = {
'': 'Z', '': 'H', '': 'Y', '': 'Y',
'': 'B', '': 'G', '': 'G', '': 'Z', '': 'T',
'': 'C', '': 'S', '': 'Y', '': 'L',
'': 'C', '': 'P', '': 'X', '': 'M',
'': 'M', '': 'K', '': 'G', '': 'L',
'': 'X', '': 'T', '': 'G', '': 'N',
'': 'Y', '': 'M', '': 'C', '': 'X',
'': 'T', '': 'J', '': 'B', '': 'J',
'': 'S', '': 'C', '': 'D', '': 'R',
'': 'D', '': 'C', '': 'P', '': 'L',
'': 'S', '': 'Z', '': 'P', '': 'Z',
'': 'Q', '': 'X', '': 'J', '': 'S',
'': 'Y', '': 'H', '': 'Z', '': 'Z',
'': 'J', '': 'H', '': 'Z', '': 'X',
'': 'B', '': 'G', '': 'T', '': 'J',
'': 'S', '': 'Y', '': 'Y', '': 'B',
'': 'Y', '': 'M', '': 'H', '': 'G',
'': 'J', '': 'C', '': 'J', '': 'K',
'': 'A', '': 'Q', '': 'R', '': 'Z',
'': 'J', '': 'K', '': 'Y', '': 'H',
'': 'X', '': 'N', '': 'S', '': 'W',
'': 'K', '': 'F', '': 'C', '': 'Y',
'': 'W', '': 'S', '': 'J', '': 'Y',
'': 'F', '': 'B', '': 'Z', '': 'G',
'': 'R', '': 'G', '': 'S', '': 'Y',
'': 'X', '': 'Z', '': 'R', '': 'G',
'': 'F', '': 'J', '': 'T', '': 'K',
'': 'J', '': 'W', '': 'L', '': 'X',
'': 'X', '': 'R', '': 'J', '': 'X',
'': 'T', '': 'J', '': 'J', '': 'F',
'': 'A', '': 'F', '': 'W', '': 'Z',
'': 'C', '': 'P', '': 'X', '': 'Z',
'': 'X', '': 'X', '': 'S', '': 'S',
'': 'C', '': 'Y', '': 'Y', '': 'G',
'': 'L', '': 'C', '': 'W', '': 'R',
'': 'L', '': 'Z', '': 'Y', '': 'X',
'': 'Z', '': 'F', '': 'L', '': 'H',
'': 'G', '': 'Z', '': 'L', '': 'A',
'': 'Q', '': 'H', '': 'J', '': 'Z',
'': 'N', '': 'M', '': 'K', '': 'Y',
'': 'M', '': 'C', '': 'K', '': 'B',
'': 'D', '': 'A', '': 'N', '': 'L',
'': 'J', '': 'T', '': 'B', '': 'C',
'': 'D', '': 'D', '': 'H', '': 'S',
'': 'S', '': 'G', '': 'L', '': 'P',
'': 'X', '': 'F', '': 'Y', '': 'D',
'': 'R', '': 'D', '': 'C', '': 'D',
'': 'Y', '': 'D', '': 'C', '': 'S',
'': 'C', '': 'F', '': 'Z', '': 'N',
'': 'T', '': 'J', '': 'Q', '': 'B',
'': 'C', '': 'Q', '': 'X', '': 'Q',
'': 'R', '': 'T', '': 'J', '': 'S',
'': 'H', '': 'P', '': 'Z', '': 'J',
'': 'J', '': 'F', '': 'H', '': 'X',
'': 'G', '': 'C', '': 'K', '': 'X',
'': 'Q', '': 'L', '': 'B', '': 'T',
'': 'J', '': 'T', '': 'B', '': 'B',
'': 'G', '': 'R', '': 'Z', '': 'B',
'': 'Z', '': 'M', '': 'S', '': 'M',
'': 'C', '': 'B', '': 'H', '': 'L',
'': 'X', '': 'Z', '': 'T', '': 'Y',
'': 'X', '': 'J', '': 'B', '': 'Q',
'': 'G', '': 'J', '': 'C', '': 'M',
'': 'S', '': 'F', '': 'J', '': 'L',
'': 'J', '': 'S', '': 'J', '': 'R',
'': 'Q', '': 'S', '': 'L', '': 'J',
'': 'E', '': 'J', '': 'G', '': 'S',
'': 'J', '': 'K', '': 'F', '': 'W',
'': 'Q', '线': 'X', '': 'C', '': 'J',
'': 'D', '': 'S', '': 'D', '': 'X',
'': 'L', '': 'X', '': 'N', '': 'W',
'': 'D', '': 'K', '': 'K', '': 'A',
'': 'Q', '': 'J', '': 'R', '': 'K',
'': 'Z', '': 'S', '': 'J', '': 'G',
'': 'X', '': 'X', '': 'B', '': 'X',
'': 'F', '': 'Y', '': 'H', '': 'Z',
'': 'G', '': 'G', '': 'Z', '': 'Y',
'': 'Z', '': 'J', '': 'C', '': 'J',
'': 'K', '': 'T', '': 'S', '': 'C',
'': 'S', '': 'Y', '': 'Z', '': 'Q',
'': 'R', '': 'H', '': 'G', '': 'Y',
'': 'S', '': 'P', '': 'X', '': 'Z',
'': 'D', '': 'Z', '': 'C', '': 'B',
'': 'Z', '': 'F', '': 'K', '': 'T',
'': 'S', '': 'J', '': 'Y', '': 'P',
'': 'D', '': 'L', '': 'Z', '': 'C',
'': 'G', '': 'G', '': 'Y', '': 'S',
'': 'J', '': 'M', '': 'Y', '': 'D'
}
return pinyin_map.get(char, 'N')
@staticmethod
def next_snapshot_version(session, case_id):

View File

@@ -183,3 +183,23 @@ class RbacDao(object):
def get_role_name_map(session):
items = session.query(Role).filter(Role.is_delete == 0, Role.status == 1).all()
return {item.id: item.name for item in items}
@staticmethod
def get_menu_permission_codes(session, menu_ids):
if not menu_ids:
return []
items = session.query(Menu.permission_code).filter(
Menu.id.in_(menu_ids),
Menu.is_delete == 0
).all()
return [item[0] for item in items if item[0]]
@staticmethod
def get_permission_ids_by_codes(session, permission_codes):
if not permission_codes:
return []
items = session.query(Permission.id).filter(
Permission.code.in_(permission_codes),
Permission.is_delete == 0
).all()
return [item[0] for item in items]

View File

@@ -0,0 +1,70 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base
from common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class AutoExecution(Base):
__tablename__ = 'auto_execution'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
execution_no = Column(String(64), nullable=False, unique=True, comment='执行编号')
trigger_type = Column(SmallInteger, nullable=False, comment='1:单条 2:计划')
project_id = Column(BigInteger, nullable=False, comment='项目id')
plan_id = Column(BigInteger, comment='计划id')
plan_round_no = Column(Integer, comment='计划轮次')
source_case_id = Column(BigInteger, comment='单条执行来源case_id')
env_code = Column(String(32), nullable=False, comment='环境编码')
run_mode = Column(SmallInteger, default=1, comment='1:串行 2:并行')
status = Column(SmallInteger, nullable=False, default=0, comment='0:待触发 1:触发中 2:排队中 3:执行中 4:成功 5:失败 6:已取消 7:触发失败 8:回调异常')
jenkins_job_name = Column(String(128), comment='Jenkins任务名称')
jenkins_queue_id = Column(BigInteger, comment='Jenkins队列id')
jenkins_build_number = Column(BigInteger, comment='Jenkins构建号')
jenkins_build_url = Column(String(512), comment='Jenkins构建地址')
console_url = Column(String(512), comment='控制台地址')
report_url = Column(String(512), comment='报告地址')
total_count = Column(Integer, default=0, comment='总数')
pending_count = Column(Integer, default=0, comment='待执行数')
running_count = Column(Integer, default=0, comment='执行中数')
passed_count = Column(Integer, default=0, comment='通过数')
failed_count = Column(Integer, default=0, comment='失败数')
blocked_count = Column(Integer, default=0, comment='阻塞数')
skipped_count = Column(Integer, default=0, comment='跳过数')
not_found_count = Column(Integer, default=0, comment='未找到数')
trigger_by = Column(BigInteger, comment='触发人')
trigger_source = Column(String(32), server_default=text("'platform'"), comment='触发来源')
trigger_message = Column(Text, comment='触发消息/失败原因')
start_time = Column(TIMESTAMP, comment='开始时间')
end_time = Column(TIMESTAMP, comment='结束时间')
duration_seconds = Column(Integer, comment='耗时秒数')
callback_token = Column(String(128), comment='回调token')
ext = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展字段')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
class AutoExecutionCase(Base):
__tablename__ = 'auto_execution_case'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
execution_id = Column(BigInteger, nullable=False, comment='执行主单id')
plan_case_id = Column(BigInteger, comment='计划用例id')
case_id = Column(BigInteger, nullable=False, comment='用例id')
case_key = Column(String(64), comment='用例编号快照')
case_title = Column(String(255), comment='用例标题快照')
run_order = Column(Integer, default=0, comment='执行顺序')
status = Column(SmallInteger, nullable=False, default=0, comment='0:待执行 1:执行中 2:通过 3:失败 4:阻塞 5:跳过 6:未找到 7:已取消')
pytest_nodeid = Column(String(512), comment='pytest节点标识')
result_message = Column(Text, comment='结果摘要')
error_message = Column(Text, comment='错误信息')
stack_trace = Column(Text, comment='堆栈')
report_url = Column(String(512), comment='单用例报告地址')
duration_seconds = Column(Integer, comment='耗时秒数')
started_time = Column(TIMESTAMP, comment='开始时间')
finished_time = Column(TIMESTAMP, comment='结束时间')
retry_count = Column(Integer, default=0, comment='重试次数')
ext = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展字段')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')

View File

@@ -20,6 +20,8 @@ class TestPlan(Base):
owner_id = Column(BigInteger, comment='负责人')
status = Column(SmallInteger, default=0, comment='0:草稿 1:进行中 2:已完成 3:已归档 4已通过')
environment_id = Column(BigInteger, comment='环境id')
jenkins_url = Column(String(512), comment='Jenkins构建URL')
is_auto = Column(SmallInteger, default=0, comment='是否自动化测试计划0-否1-是')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')

View File

@@ -0,0 +1,521 @@
# encoding: UTF-8
import secrets
from datetime import datetime
from ..dao.automationDao import AutomationDao
from ..model.automationModel import AutoExecution, AutoExecutionCase
from ..model.planModel import PlanCase
from ..service.planService import PlanService
from common.jenkinsRequest import JenkinsRequest
from const import JENKINS_DEFAULT_JOB, PLATFORM_BASE_URL
from logger import logger
class AutomationService(object):
STATUS_PENDING = 0
STATUS_TRIGGERING = 1
STATUS_QUEUED = 2
STATUS_RUNNING = 3
STATUS_SUCCESS = 4
STATUS_FAILED = 5
STATUS_CANCELED = 6
STATUS_TRIGGER_FAILED = 7
STATUS_CALLBACK_ERROR = 8
CASE_STATUS_PENDING = 0
CASE_STATUS_RUNNING = 1
CASE_STATUS_PASSED = 2
CASE_STATUS_FAILED = 3
CASE_STATUS_BLOCKED = 4
CASE_STATUS_SKIPPED = 5
CASE_STATUS_NOT_FOUND = 6
CASE_STATUS_CANCELED = 7
PLAN_CASE_STATUS_MAP = {
CASE_STATUS_PASSED: 1,
CASE_STATUS_FAILED: 2,
CASE_STATUS_BLOCKED: 3,
}
@staticmethod
def generate_execution_no():
return 'AE' + datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3]
@staticmethod
def generate_callback_token():
return secrets.token_hex(16)
@staticmethod
def create_case_execution(session, req_data, user_id):
case_id = req_data.get('caseId') or req_data.get('case_id')
env_code = req_data.get('envCode') or req_data.get('env_code')
if not case_id or not env_code:
return {}, 'caseId、envCode 为必传参数'
case_item = AutomationDao.query_case_auto_item(session, case_id)
if not case_item:
return {}, '该用例不存在或未接入自动化'
running_exists = AutomationService.get_running_execution_by_case(session, case_id, env_code)
if running_exists:
return {}, '该用例在当前环境已有执行中任务'
callback_token = AutomationService.generate_callback_token()
execution_obj, err_msg = AutomationDao.create_execution(session, {
'execution_no': AutomationService.generate_execution_no(),
'trigger_type': 1,
'project_id': case_item.project_id,
'source_case_id': case_item.id,
'env_code': env_code,
'run_mode': int(req_data.get('runMode') or req_data.get('run_mode') or 1),
'status': AutomationService.STATUS_PENDING,
'total_count': 1,
'pending_count': 1,
'running_count': 0,
'passed_count': 0,
'failed_count': 0,
'blocked_count': 0,
'skipped_count': 0,
'not_found_count': 0,
'trigger_by': user_id,
'trigger_message': req_data.get('remark'),
'callback_token': callback_token,
'ext': {}
})
if err_msg:
return {}, err_msg
_, err_msg = AutomationDao.batch_create_execution_cases(session, [{
'execution_id': execution_obj.id,
'case_id': case_item.id,
'case_key': case_item.case_key,
'case_title': case_item.title,
'run_order': 1,
'status': AutomationService.CASE_STATUS_PENDING
}])
if err_msg:
return {}, err_msg
trigger_ok, trigger_msg = AutomationService.trigger_jenkins(session, execution_obj.id, req_data.get('jenkinsJobName'))
if not trigger_ok:
return {}, trigger_msg
execution = AutomationDao.get_execution_by_id(session, execution_obj.id)
return execution.to_dict() if execution else {'id': execution_obj.id}, ''
@staticmethod
def create_plan_execution(session, req_data, user_id):
from logger import logger
plan_id = req_data.get('planId') or req_data.get('plan_id')
env_code = req_data.get('envCode') or req_data.get('env_code')
round_no = req_data.get('roundNo') or req_data.get('round_no')
case_ids = req_data.get('caseIds') or req_data.get('case_ids') or []
logger.info(f'====== 计划自动化执行开始 ======')
logger.info(f'请求参数: plan_id={plan_id}, env_code={env_code}, round_no={round_no}, case_ids={case_ids}, user_id={user_id}')
if not plan_id or not env_code:
logger.error('参数校验失败: planId、envCode 为必传参数')
return {}, 'planId、envCode 为必传参数'
running_exists = AutomationService.get_running_execution_by_plan(session, plan_id, env_code)
if running_exists:
logger.error(f'计划执行冲突: 计划 {plan_id} 在环境 {env_code} 已有执行中任务')
return {}, '该计划在当前环境已有执行中任务'
logger.info(f'查询计划自动化用例: plan_id={plan_id}, round_no={round_no}')
items = AutomationDao.query_plan_auto_cases(session, plan_id, round_no, case_ids)
if not items:
logger.error('计划下无可执行自动化用例')
return {}, '计划下无可执行自动化用例'
logger.info(f'查询到 {len(items)} 条自动化用例')
for idx, (plan_case, case_item) in enumerate(items, start=1):
logger.info(f' {idx}. case_key={case_item.case_key}, case_title={case_item.title}')
project_id = items[0][1].project_id
callback_token = AutomationService.generate_callback_token()
execution_no = AutomationService.generate_execution_no()
logger.info(f'创建执行记录: execution_no={execution_no}, project_id={project_id}, plan_id={plan_id}')
execution_obj, err_msg = AutomationDao.create_execution(session, {
'execution_no': execution_no,
'trigger_type': 2,
'project_id': project_id,
'plan_id': int(plan_id),
'plan_round_no': int(round_no) if round_no not in (None, '') else None,
'env_code': env_code,
'run_mode': int(req_data.get('runMode') or req_data.get('run_mode') or 1),
'status': AutomationService.STATUS_PENDING,
'total_count': len(items),
'pending_count': len(items),
'running_count': 0,
'passed_count': 0,
'failed_count': 0,
'blocked_count': 0,
'skipped_count': 0,
'not_found_count': 0,
'trigger_by': user_id,
'trigger_message': req_data.get('remark'),
'callback_token': callback_token,
'ext': {}
})
if err_msg:
logger.error(f'创建执行记录失败: {err_msg}')
return {}, err_msg
logger.info(f'执行记录创建成功: execution_id={execution_obj.id}')
batch_list = []
for idx, (plan_case, case_item) in enumerate(items, start=1):
batch_list.append({
'execution_id': execution_obj.id,
'plan_case_id': plan_case.id,
'case_id': case_item.id,
'case_key': case_item.case_key,
'case_title': case_item.title,
'run_order': idx,
'status': AutomationService.CASE_STATUS_PENDING
})
logger.info(f'批量创建执行明细: {len(batch_list)}')
_, err_msg = AutomationDao.batch_create_execution_cases(session, batch_list)
if err_msg:
logger.error(f'批量创建执行明细失败: {err_msg}')
return {}, err_msg
logger.info(f'触发Jenkins构建: execution_id={execution_obj.id}')
trigger_ok, trigger_msg = AutomationService.trigger_jenkins(session, execution_obj.id, req_data.get('jenkinsJobName'))
if not trigger_ok:
logger.error(f'Jenkins触发失败: {trigger_msg}')
return {}, trigger_msg
logger.info('计划自动化执行成功')
execution = AutomationDao.get_execution_by_id(session, execution_obj.id)
logger.info(f'====== 计划自动化执行结束 ======')
return execution.to_dict() if execution else {'id': execution_obj.id}, ''
@staticmethod
def trigger_jenkins(session, execution_id, job_name=None):
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return False, '未查询到对应执行记录'
AutomationDao.update_execution_by_id(session, execution_id, {'status': AutomationService.STATUS_TRIGGERING})
cases, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id)], 1, 100000)
case_keys = [case.case_key for case in cases if case.case_key]
test_target = ','.join(case_keys)
test_type = 'story' if case_keys else 'all'
params = {
'EXECUTION_ID': execution.id,
'CALLBACK_TOKEN': execution.callback_token,
'PLATFORM_BASE_URL': PLATFORM_BASE_URL,
'ENV_CODE': execution.env_code,
'RUN_MODE': execution.run_mode,
'TRIGGER_TYPE': execution.trigger_type,
'TEST_TYPE': test_type,
'TEST_TARGET': test_target
}
jenkins_url = None
jenkins_job_name = None
if execution.plan_id:
plan = AutomationDao.get_plan_by_id(session, execution.plan_id)
if plan and plan.jenkins_url:
jenkins_url = plan.jenkins_url
if '/job/' in jenkins_url:
import re
match = re.match(r'^(https?://[^/]+)/(job/[^/]+)/?.*$', jenkins_url)
if match:
jenkins_url = match.group(1)
jenkins_job_name = match.group(2).replace('job/', '')
logger.info(f'从计划配置中解析 Jenkins: base_url={jenkins_url}, job_name={jenkins_job_name}')
jenkins_request = JenkinsRequest(jenkins_url=jenkins_url)
target_job_name = jenkins_job_name or job_name or execution.jenkins_job_name or JENKINS_DEFAULT_JOB
success, err_msg, payload = jenkins_request.build_with_parameters(params, target_job_name)
if not success:
AutomationDao.update_execution_by_id(session, execution_id, {
'status': AutomationService.STATUS_TRIGGER_FAILED,
'trigger_message': err_msg
})
return False, err_msg
update_info = {
'status': AutomationService.STATUS_QUEUED,
'jenkins_job_name': payload.get('job_name') or job_name or JENKINS_DEFAULT_JOB,
'jenkins_queue_id': payload.get('queue_id')
}
if payload.get('location'):
update_info['trigger_message'] = payload.get('location')
AutomationDao.update_execution_by_id(session, execution_id, update_info)
return True, ''
@staticmethod
def get_running_execution_by_case(session, case_id, env_code):
items, _ = AutomationDao.list_execution_by_filters(session, [
AutoExecution.source_case_id == int(case_id),
AutoExecution.env_code == env_code,
AutoExecution.status.in_([0, 1, 2, 3])
], 1, 1)
return items[0] if items else None
@staticmethod
def get_running_execution_by_plan(session, plan_id, env_code):
items, _ = AutomationDao.list_execution_by_filters(session, [
AutoExecution.plan_id == int(plan_id),
AutoExecution.env_code == env_code,
AutoExecution.status.in_([0, 1, 2, 3])
], 1, 1)
return items[0] if items else None
@staticmethod
def list_executions(session, req_data):
filters = []
project_id = req_data.get('projectId') or req_data.get('project_id')
plan_id = req_data.get('planId') or req_data.get('plan_id')
status = req_data.get('status')
trigger_type = req_data.get('triggerType') or req_data.get('trigger_type')
if project_id:
filters.append(AutoExecution.project_id == int(project_id))
if plan_id:
filters.append(AutoExecution.plan_id == int(plan_id))
if status not in (None, ''):
filters.append(AutoExecution.status == int(status))
if trigger_type not in (None, ''):
filters.append(AutoExecution.trigger_type == int(trigger_type))
items, total = AutomationDao.list_execution_by_filters(session, filters, req_data.get('pageNo') or req_data.get('page') or 1, req_data.get('pageSize') or req_data.get('size') or 20)
return {'list': [item.to_dict() for item in items], 'total': total}
@staticmethod
def get_execution_detail(session, execution_id):
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return {}, '未查询到对应执行记录'
ret = execution.to_dict()
summary = AutomationDao.count_execution_case_summary(session, execution_id)
ret.update({
'summary': {
'total': summary.get('total', 0),
'pending': summary.get(0, 0),
'running': summary.get(1, 0),
'passed': summary.get(2, 0),
'failed': summary.get(3, 0),
'blocked': summary.get(4, 0),
'skipped': summary.get(5, 0),
'notFound': summary.get(6, 0),
'canceled': summary.get(7, 0)
}
})
return ret, ''
@staticmethod
def list_execution_cases(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
if not execution_id:
return {}, 'executionId 为必传参数'
filters = [AutoExecutionCase.execution_id == int(execution_id)]
status = req_data.get('status')
if status not in (None, ''):
filters.append(AutoExecutionCase.status == int(status))
items, total = AutomationDao.list_execution_case_by_filters(session, filters, req_data.get('pageNo') or req_data.get('page') or 1, req_data.get('pageSize') or req_data.get('size') or 20)
return {'list': [item.to_dict() for item in items], 'total': total}, ''
@staticmethod
def pull_execution_cases(session, execution_id, callback_token):
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return {}, '未查询到对应执行记录'
if execution.callback_token != callback_token:
return {}, '回调鉴权失败'
case_items, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id)], 1, 100000)
return {
'executionId': execution.id,
'executionNo': execution.execution_no,
'triggerType': execution.trigger_type,
'projectId': execution.project_id,
'planId': execution.plan_id,
'envCode': execution.env_code,
'runMode': execution.run_mode,
'items': [{
'executionCaseId': item.id,
'planCaseId': item.plan_case_id,
'caseId': item.case_id,
'caseKey': item.case_key,
'caseTitle': item.case_title,
'runOrder': item.run_order
} for item in case_items]
}, ''
@staticmethod
def mark_execution_queued(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return 0, '未查询到对应执行记录'
return AutomationDao.update_execution_by_id(session, execution_id, {
'status': AutomationService.STATUS_QUEUED,
'jenkins_queue_id': req_data.get('queueId') or req_data.get('queue_id'),
'jenkins_job_name': req_data.get('jobName') or req_data.get('job_name') or execution.jenkins_job_name,
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number'),
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url')
})
@staticmethod
def mark_execution_started(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return 0, '未查询到对应执行记录'
start_time = req_data.get('startTime') or req_data.get('start_time') or datetime.now()
return AutomationDao.update_execution_by_id(session, execution_id, {
'status': AutomationService.STATUS_RUNNING,
'jenkins_job_name': req_data.get('jobName') or req_data.get('job_name') or execution.jenkins_job_name,
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number'),
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url'),
'console_url': req_data.get('consoleUrl') or req_data.get('console_url'),
'start_time': start_time
})
@staticmethod
def save_case_result(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
execution_case_id = req_data.get('executionCaseId') or req_data.get('execution_case_id')
case_id = req_data.get('caseId') or req_data.get('case_id')
if not execution_id or (not execution_case_id and not case_id):
return 0, 'executionId、executionCaseId/caseId 为必传参数'
execution_case = AutomationDao.get_execution_case_by_id(session, execution_case_id) if execution_case_id else None
if not execution_case and case_id:
execution_case = AutomationDao.get_execution_case_by_unique(session, execution_id, case_id, req_data.get('planCaseId') or req_data.get('plan_case_id'))
if not execution_case:
return 0, '未查询到对应执行明细'
update_info = {
'status': int(req_data.get('status')) if req_data.get('status') is not None else execution_case.status,
'pytest_nodeid': req_data.get('pytestNodeid') or req_data.get('pytest_nodeid'),
'result_message': req_data.get('resultMessage') or req_data.get('result_message'),
'error_message': req_data.get('errorMessage') or req_data.get('error_message'),
'stack_trace': req_data.get('stackTrace') or req_data.get('stack_trace'),
'report_url': req_data.get('reportUrl') or req_data.get('report_url'),
'duration_seconds': req_data.get('durationSeconds') or req_data.get('duration_seconds'),
'started_time': req_data.get('startedTime') or req_data.get('started_time') or execution_case.started_time,
'finished_time': req_data.get('finishedTime') or req_data.get('finished_time') or datetime.now(),
'ext': req_data.get('ext') if req_data.get('ext') is not None else execution_case.ext
}
update_id, err_msg = AutomationDao.update_execution_case_by_id(session, execution_case.id, update_info)
if err_msg:
return update_id, err_msg
execution_case = AutomationDao.get_execution_case_by_id(session, execution_case.id)
if execution_case and execution_case.plan_case_id:
AutomationService.sync_plan_case_result(session, execution_case)
AutomationService.refresh_execution_summary(session, execution_id)
execution = AutomationDao.get_execution_by_id(session, execution_id)
if execution and execution.plan_id:
AutomationService.refresh_plan_status(session, execution.plan_id)
return execution_case.id, ''
@staticmethod
def finish_execution(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return 0, '未查询到对应执行记录'
end_time = req_data.get('endTime') or req_data.get('end_time') or datetime.now()
start_time = req_data.get('startTime') or req_data.get('start_time') or execution.start_time
update_info = {
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number') or execution.jenkins_build_number,
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url') or execution.jenkins_build_url,
'console_url': req_data.get('consoleUrl') or req_data.get('console_url') or execution.console_url,
'report_url': req_data.get('reportUrl') or req_data.get('report_url') or execution.report_url,
'start_time': start_time,
'end_time': end_time,
'duration_seconds': req_data.get('durationSeconds') or req_data.get('duration_seconds') or AutomationService.calc_duration_seconds(start_time, end_time)
}
update_id, err_msg = AutomationDao.update_execution_by_id(session, execution_id, update_info)
if err_msg:
return update_id, err_msg
AutomationService.refresh_execution_summary(session, execution_id, force_finish=True)
execution = AutomationDao.get_execution_by_id(session, execution_id)
if execution and execution.plan_id:
AutomationService.refresh_plan_status(session, execution.plan_id)
return int(execution_id), ''
@staticmethod
def abort_execution(session, req_data):
execution_id = req_data.get('executionId') or req_data.get('execution_id')
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return 0, '未查询到对应执行记录'
case_items, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id), AutoExecutionCase.status.in_([0, 1])], 1, 100000)
for item in case_items:
AutomationDao.update_execution_case_by_id(session, item.id, {'status': AutomationService.CASE_STATUS_CANCELED, 'finished_time': datetime.now()})
update_id, err_msg = AutomationDao.update_execution_by_id(session, execution_id, {
'status': int(req_data.get('status') or AutomationService.STATUS_CANCELED),
'trigger_message': req_data.get('message') or req_data.get('trigger_message'),
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number') or execution.jenkins_build_number,
'console_url': req_data.get('consoleUrl') or req_data.get('console_url') or execution.console_url,
'end_time': datetime.now()
})
if err_msg:
return update_id, err_msg
AutomationService.refresh_execution_summary(session, execution_id, keep_terminal_status=True)
execution = AutomationDao.get_execution_by_id(session, execution_id)
if execution and execution.plan_id:
AutomationService.refresh_plan_status(session, execution.plan_id)
return int(execution_id), ''
@staticmethod
def sync_plan_case_result(session, execution_case):
status = AutomationService.PLAN_CASE_STATUS_MAP.get(execution_case.status)
update_info = {
'actual_result': execution_case.error_message or execution_case.result_message,
'executed_time': execution_case.finished_time or datetime.now(),
'execution_duration': execution_case.duration_seconds
}
if status is not None:
update_info['status'] = status
AutomationDao.update_plan_case_result(session, execution_case.plan_case_id, update_info)
@staticmethod
def refresh_execution_summary(session, execution_id, force_finish=False, keep_terminal_status=False):
summary = AutomationDao.count_execution_case_summary(session, execution_id)
execution = AutomationDao.get_execution_by_id(session, execution_id)
if not execution:
return
update_info = {
'total_count': summary.get('total', 0),
'pending_count': summary.get(0, 0),
'running_count': summary.get(1, 0),
'passed_count': summary.get(2, 0),
'failed_count': summary.get(3, 0),
'blocked_count': summary.get(4, 0),
'skipped_count': summary.get(5, 0),
'not_found_count': summary.get(6, 0)
}
total = summary.get('total', 0)
running_count = summary.get(1, 0)
finished_count = summary.get(2, 0) + summary.get(3, 0) + summary.get(4, 0) + summary.get(5, 0) + summary.get(6, 0) + summary.get(7, 0)
if not keep_terminal_status:
if running_count > 0:
update_info['status'] = AutomationService.STATUS_RUNNING
elif total > 0 and finished_count == total:
if summary.get(3, 0) + summary.get(4, 0) + summary.get(6, 0) > 0:
update_info['status'] = AutomationService.STATUS_FAILED
else:
update_info['status'] = AutomationService.STATUS_SUCCESS
if force_finish or (total > 0 and finished_count == total):
end_time = execution.end_time or datetime.now()
update_info['end_time'] = end_time
if execution.start_time:
update_info['duration_seconds'] = AutomationService.calc_duration_seconds(execution.start_time, end_time)
AutomationDao.update_execution_by_id(session, execution_id, update_info)
@staticmethod
def refresh_plan_status(session, plan_id):
PlanService.refresh_plan_status(session, plan_id)
@staticmethod
def calc_duration_seconds(start_time, end_time):
if not start_time or not end_time:
return None
if isinstance(start_time, str):
start_time = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S')
if isinstance(end_time, str):
end_time = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S')
return int((end_time - start_time).total_seconds())

View File

@@ -26,8 +26,8 @@ class CaseService(object):
return CaseDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def next_case_key(session, project_id):
return CaseDao.next_case_key(session, project_id)
def next_case_key(session, project_id, module_id=None, product_id=None):
return CaseDao.next_case_key(session, project_id, module_id, product_id)
@staticmethod
def next_snapshot_version(session, case_id):

View File

@@ -0,0 +1,302 @@
# encoding: UTF-8
import json
import time
from datetime import datetime
import requests
from requests.auth import HTTPBasicAuth
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from const import JENKINS_BASE_URL, JENKINS_USER, JENKINS_TOKEN
from logger import logger
from app.api.model.automationModel import AutoExecution, AutoExecutionCase
class JenkinsPollService(object):
STATUS_QUEUED = 2
STATUS_RUNNING = 3
STATUS_SUCCESS = 4
STATUS_FAILED = 5
@staticmethod
def poll_jenkins_build_status(session, execution_id):
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
if not execution:
logger.error(f'执行记录不存在: execution_id={execution_id}')
return False, '执行记录不存在'
if execution.status not in [JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING]:
logger.info(f'执行状态不需要轮询: execution_id={execution_id}, status={execution.status}')
return True, ''
base_url = JENKINS_BASE_URL.rstrip('/')
job_name = execution.jenkins_job_name
build_number = execution.jenkins_build_number
if not job_name:
if execution.jenkins_build_url:
import re
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/\d+/', execution.jenkins_build_url)
if match:
job_name = match.group(1).replace('/job/', '/')
logger.info(f'从构建URL中提取job_name: {job_name}')
else:
logger.error(f'无法从构建URL中提取job_name: {execution.jenkins_build_url}')
return False, 'Jenkins job 名称为空'
else:
logger.error(f'Jenkins job 名称为空: execution_id={execution_id}')
return False, 'Jenkins job 名称为空'
auth = HTTPBasicAuth(JENKINS_USER, JENKINS_TOKEN) if JENKINS_USER and JENKINS_TOKEN else None
try:
if not build_number:
if execution.jenkins_build_url:
import re
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/(\d+)/', execution.jenkins_build_url)
if match:
job_name = match.group(1).replace('/job/', '/')
build_number = match.group(2)
logger.info(f'从构建URL中提取: job_name={job_name}, build_number={build_number}')
else:
logger.error(f'无法从构建URL中提取信息: {execution.jenkins_build_url}')
queue_id = execution.jenkins_queue_id
if queue_id:
queue_url = f'{base_url}/queue/item/{queue_id}/api/json'
response = requests.get(queue_url, auth=auth, timeout=30)
if response.status_code == 200:
queue_data = response.json()
logger.debug(f'队列数据: execution_id={execution_id}, queue_data={json.dumps(queue_data, ensure_ascii=False)[:500]}')
if queue_data.get('executable'):
build_number = queue_data['executable'].get('number')
logger.info(f'队列任务已开始执行: execution_id={execution_id}, build_number={build_number}')
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
'jenkins_build_number': build_number,
'status': JenkinsPollService.STATUS_RUNNING,
'start_time': datetime.now()
})
session.done(close=False)
elif queue_data.get('cancelled') or queue_data.get('blocked'):
logger.error(f'队列任务已取消或阻塞: execution_id={execution_id}, cancelled={queue_data.get("cancelled")}, blocked={queue_data.get("blocked")}')
end_time = datetime.now()
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
'status': JenkinsPollService.STATUS_FAILED,
'end_time': end_time,
'trigger_message': queue_data.get('why', '队列任务已取消或阻塞')
})
session.done(close=False)
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
if execution.plan_id:
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
return True, '队列任务已取消或阻塞'
elif queue_data.get('why'):
logger.info(f'队列任务等待中: execution_id={execution_id}, reason={queue_data.get("why")}')
return True, f'队列等待中: {queue_data.get("why")}'
else:
logger.info(f'队列任务等待中: execution_id={execution_id}, queue_id={queue_id}')
return True, '队列等待中'
else:
logger.warning(f'获取队列状态失败: execution_id={execution_id}, status_code={response.status_code}')
if response.status_code == 404:
logger.info(f'队列项已不存在,尝试查询执行状态: execution_id={execution_id}')
builds_url = f'{base_url}/job/{job_name}/builds/api/json?limit=10'
try:
builds_response = requests.get(builds_url, auth=auth, timeout=30)
logger.info(f'构建历史查询: url={builds_url}, status_code={builds_response.status_code}')
if builds_response.status_code == 200:
builds_data = builds_response.json()
logger.info(f'构建历史数据: count={len(builds_data) if builds_data else 0}')
if builds_data:
latest_build = builds_data[0]
build_number = latest_build.get('number')
is_building = latest_build.get('building', False)
result = latest_build.get('result')
timestamp = latest_build.get('timestamp', 0)
logger.info(f'最新构建信息: build_number={build_number}, is_building={is_building}, result={result}')
if is_building:
status = JenkinsPollService.STATUS_RUNNING
elif result == 'SUCCESS':
status = JenkinsPollService.STATUS_SUCCESS
else:
status = JenkinsPollService.STATUS_FAILED
logger.info(f'更新执行状态: execution_id={execution_id}, build_number={build_number}, status={status}')
update_info = {
'jenkins_build_number': build_number,
'status': status,
'start_time': datetime.fromtimestamp(timestamp/1000) if timestamp else datetime.now()
}
if not is_building and result:
update_info['end_time'] = datetime.now()
update_info['jenkins_build_url'] = f'{base_url}/job/{job_name}/{build_number}/'
update_info['console_url'] = f'{base_url}/job/{job_name}/{build_number}/console'
update_info['report_url'] = f'{base_url}/job/{job_name}/{build_number}/allure/'
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
session.done(close=False)
if not is_building:
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
if execution.plan_id:
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
return True, f'队列不存在,使用最新构建: {build_number}'
else:
logger.error(f'获取构建历史失败: status_code={builds_response.status_code}, body={builds_response.text[:200]}')
except Exception as err:
logger.error(f'查询构建历史异常: {err}')
return True, '获取队列状态失败'
else:
logger.warning(f'缺少 queue_id 和 build_number: execution_id={execution_id}')
return False, '无法轮询,缺少构建信息'
if build_number:
build_url = f'{base_url}/job/{job_name}/{build_number}/api/json'
response = requests.get(build_url, auth=auth, timeout=30)
if response.status_code == 200:
build_data = response.json()
is_running = build_data.get('building', False)
result = build_data.get('result')
console_url = f'{base_url}/job/{job_name}/{build_number}/console'
build_url_full = f'{base_url}/job/{job_name}/{build_number}/'
if is_running:
logger.info(f'构建执行中: execution_id={execution_id}, build_number={build_number}')
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
'status': JenkinsPollService.STATUS_RUNNING,
'jenkins_build_url': build_url_full,
'console_url': console_url
})
session.done(close=False)
return True, '执行中'
else:
logger.info(f'构建完成: execution_id={execution_id}, result={result}')
end_time = datetime.now()
report_url = f'{base_url}/job/{job_name}/{build_number}/allure/'
update_info = {
'status': JenkinsPollService.STATUS_SUCCESS if result == 'SUCCESS' else JenkinsPollService.STATUS_FAILED,
'jenkins_build_url': build_url_full,
'console_url': console_url,
'report_url': report_url,
'end_time': end_time
}
if execution.start_time:
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
session.done(close=False)
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
if execution.plan_id:
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
return True, f'构建完成: {result}'
except Exception as err:
logger.error(f'轮询 Jenkins 状态失败: execution_id={execution_id}, error={err}')
return False, str(err)
return True, ''
@staticmethod
def refresh_execution_summary(session, execution_id, force_finish=False):
from sqlalchemy import func
rows = session.query(AutoExecutionCase.status, func.count(AutoExecutionCase.id)).filter(
AutoExecutionCase.execution_id == int(execution_id)
).group_by(AutoExecutionCase.status).all()
summary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
for status, count in rows:
summary[int(status)] = int(count)
total = sum(summary.values())
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
if execution:
update_info = {
'total_count': total,
'pending_count': summary.get(0, 0),
'running_count': summary.get(1, 0),
'passed_count': summary.get(2, 0),
'failed_count': summary.get(3, 0),
'blocked_count': summary.get(4, 0),
'skipped_count': summary.get(5, 0),
'not_found_count': summary.get(6, 0)
}
running_count = summary.get(1, 0)
finished_count = summary.get(2, 0) + summary.get(3, 0) + summary.get(4, 0) + summary.get(5, 0) + summary.get(6, 0) + summary.get(7, 0)
if running_count > 0:
update_info['status'] = JenkinsPollService.STATUS_RUNNING
elif total > 0 and finished_count == total:
if summary.get(3, 0) + summary.get(4, 0) + summary.get(6, 0) > 0:
update_info['status'] = JenkinsPollService.STATUS_FAILED
else:
update_info['status'] = JenkinsPollService.STATUS_SUCCESS
if force_finish or (total > 0 and finished_count == total):
end_time = execution.end_time or datetime.now()
update_info['end_time'] = end_time
if execution.start_time:
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
session.done(close=False)
@staticmethod
def refresh_plan_status(session, plan_id):
from sqlalchemy import func
rows = session.query(
AutoExecution.status, func.count(AutoExecution.id)
).filter(
AutoExecution.plan_id == int(plan_id),
AutoExecution.status.in_([JenkinsPollService.STATUS_RUNNING, JenkinsPollService.STATUS_SUCCESS, JenkinsPollService.STATUS_FAILED])
).group_by(AutoExecution.status).all()
status_counts = {}
for status, count in rows:
status_counts[status] = count
running_count = status_counts.get(JenkinsPollService.STATUS_RUNNING, 0)
success_count = status_counts.get(JenkinsPollService.STATUS_SUCCESS, 0)
failed_count = status_counts.get(JenkinsPollService.STATUS_FAILED, 0)
from app.api.model.planModel import TestPlan
if running_count > 0:
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 1})
elif success_count > 0 and failed_count == 0:
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 4})
elif success_count + failed_count > 0:
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 2})
session.done(close=False)
@staticmethod
def poll_all_pending_executions(session):
pending_executions = session.query(AutoExecution).filter(
AutoExecution.status.in_([JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING])
).all()
for execution in pending_executions:
try:
success, msg = JenkinsPollService.poll_jenkins_build_status(session, execution.id)
logger.info(f'轮询执行 {execution.id}: success={success}, msg={msg}')
except Exception as err:
logger.error(f'轮询执行 {execution.id} 异常: {err}')
session.done(close=False)

View File

@@ -1,5 +1,6 @@
# encoding: UTF-8
from ..dao.planDao import PlanDao
from ..model.planModel import PlanCase, TestPlan
class PlanService(object):
@@ -32,3 +33,22 @@ class PlanService(object):
@staticmethod
def plan_stats(session, plan_id):
return PlanDao.plan_stats(session, plan_id)
@staticmethod
def refresh_plan_status(session, plan_id):
total = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id)).count()
if total == 0:
return
unexecuted_count = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 0).count()
failed_count = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id), PlanCase.status.in_([2, 3])).count()
plan = PlanDao.get_by_id(session, TestPlan, plan_id)
if not plan or plan.status == 3:
return
if unexecuted_count == 0:
new_status = 4 if failed_count == 0 else 2
elif unexecuted_count < total:
new_status = 1
else:
new_status = plan.status
if new_status != plan.status:
PlanDao.update_by_id(session, TestPlan, plan_id, {'status': new_status})

View File

@@ -59,6 +59,14 @@ class RbacService(object):
def get_role_menu_ids(session, role_id):
return RbacDao.get_role_menu_ids(session, role_id)
@staticmethod
def get_menu_permission_codes(session, menu_ids):
return RbacDao.get_menu_permission_codes(session, menu_ids)
@staticmethod
def get_permission_ids_by_codes(session, permission_codes):
return RbacDao.get_permission_ids_by_codes(session, permission_codes)
@staticmethod
def build_menu_tree(session, filters, role_ids=None, menu_ids=None):
items = RbacDao.get_menu_tree_items(session, filters)

View File

@@ -16,10 +16,22 @@ from common.sqlSession import SqlSession
TOKEN_PREFIX = 'effekt:token:'
TOKEN_CONTEXT_PREFIX = 'effekt:token:ctx:'
REFRESH_TOKEN_PREFIX = 'effekt:refresh:'
TOKEN_EXPIRE_SECONDS = 7200
REFRESH_TOKEN_EXPIRE_SECONDS = 86400 * 7
TOKEN_REFRESH_THRESHOLD_SECONDS = 1800
TOKEN_CONTEXT_EXPIRE_SECONDS = 300
WHITELIST_PATHS = ['/it/api/auth/login', '/it/api/auth/register']
WHITELIST_PATHS = [
'/it/api/auth/login',
'/it/api/auth/register',
'/it/api/auth/refresh',
'/it/api/automation/execution/case/pull',
'/it/api/automation/execution/queued',
'/it/api/automation/execution/start',
'/it/api/automation/execution/case/result',
'/it/api/automation/execution/finish',
'/it/api/automation/execution/abort'
]
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
_redis_client.ping()
@@ -32,6 +44,34 @@ def create_token(user_id):
return token, TOKEN_EXPIRE_SECONDS
def create_refresh_token(user_id):
refresh_token = uuid.uuid4().hex
key = REFRESH_TOKEN_PREFIX + refresh_token
_redis_client.setex(key, REFRESH_TOKEN_EXPIRE_SECONDS, str(user_id))
return refresh_token, REFRESH_TOKEN_EXPIRE_SECONDS
def validate_refresh_token(refresh_token):
key = REFRESH_TOKEN_PREFIX + refresh_token
user_id = _redis_client.get(key)
if user_id:
return int(user_id)
return None
def revoke_refresh_token(refresh_token):
if refresh_token:
_redis_client.delete(REFRESH_TOKEN_PREFIX + refresh_token)
def revoke_all_refresh_tokens(user_id):
keys = _redis_client.keys(REFRESH_TOKEN_PREFIX + '*')
for key in keys:
stored_user_id = _redis_client.get(key)
if stored_user_id == str(user_id):
_redis_client.delete(key)
def get_token_ttl(token):
return _redis_client.ttl(TOKEN_PREFIX + token)
@@ -71,10 +111,10 @@ def login_required(func):
def wrapper(*args, **kwargs):
token = parse_token()
if not token:
return ApiResponse.build_failure(40004, msg='未登录或缺少token')
return ApiResponse.build_failure(40001, msg='缺少token')
user_id = get_current_user_id(token)
if not user_id:
return ApiResponse.build_failure(40004, msg='token无效或已过期')
return ApiResponse.build_failure(451, msg='token无效或已过期')
session = None
try:
token_context = get_token_context(token)
@@ -133,10 +173,10 @@ def permission_required(permission_code):
@wraps(func)
def wrapper(*args, **kwargs):
if not getattr(g, 'current_user_id', None):
return ApiResponse.build_failure(40004, msg='未登录或缺少token')
return ApiResponse.build_failure(40001, msg='缺少token')
current_permission_codes = getattr(g, 'current_permission_codes', [])
if not has_permission(permission_code, current_permission_codes):
return ApiResponse.build_failure(40004, msg='无权限访问该接口!')
return ApiResponse.build_failure(40003, msg='无权限访问该接口!')
return func(*args, **kwargs)
return wrapper
return decorator

View File

@@ -17,6 +17,7 @@ from .controller.rbacController import RbacController
from .controller.userController import UserController
from .controller.bugController import BugController, BugUploadController
from .controller.projectHookController import ProjectHookController
from .controller.automationController import AutomationController
api = Blueprint('api', __name__)
@@ -27,7 +28,7 @@ def api_before_request():
return None
token = request.headers.get('accessToken') or request.headers.get('accesstoken') or request.headers.get('Authorization')
if not token:
return ApiResponse.build_failure(40004, msg='未登录或缺少token')
return ApiResponse.build_failure(40001, msg='缺少token')
return None
@@ -728,6 +729,199 @@ def plan_progress():
return ApiResponse.build_success(20000, data=ret)
@api.route('/automation/case/run', methods=['POST'])
@login_required
@permission_required('automation:run')
def automation_case_run():
controller = AutomationController(request.get_json() or {})
try:
ret, err_msg = controller.case_run()
if err_msg:
return ApiResponse.build_failure(40009, msg=err_msg)
return ApiResponse.build_success(20000, data=ret)
finally:
controller.close_session()
@api.route('/automation/plan/run', methods=['POST'])
@login_required
@permission_required('automation:run')
def automation_plan_run():
controller = AutomationController(request.get_json() or {})
try:
ret, err_msg = controller.plan_run()
if err_msg:
return ApiResponse.build_failure(40009, msg=err_msg)
return ApiResponse.build_success(20000, data=ret)
finally:
controller.close_session()
@api.route('/automation/execution/list', methods=['GET'])
@login_required
@permission_required('automation:list')
def automation_execution_list():
controller = AutomationController(request.args)
try:
return ApiResponse.build_success(20000, data=controller.execution_list())
finally:
controller.close_session()
@api.route('/automation/execution/detail', methods=['GET'])
@login_required
@permission_required('automation:detail')
def automation_execution_detail():
controller = AutomationController(request.args)
try:
ret, err_msg = controller.execution_detail()
if err_msg:
return ApiResponse.build_failure(40011, msg=err_msg)
return ApiResponse.build_success(20000, data=ret)
finally:
controller.close_session()
@api.route('/automation/execution/case/list', methods=['GET'])
@login_required
@permission_required('automation:detail')
def automation_execution_case_list():
controller = AutomationController(request.args)
try:
ret, err_msg = controller.execution_case_list()
if err_msg:
return ApiResponse.build_failure(40011, msg=err_msg)
return ApiResponse.build_success(20000, data=ret)
finally:
controller.close_session()
@api.route('/automation/execution/poll', methods=['POST'])
@login_required
@permission_required('automation:detail')
def automation_execution_poll():
from ..api.service.jenkinsPollService import JenkinsPollService
from ..api.dao.automationDao import AutomationDao
req_data = request.get_json() or {}
execution_id = req_data.get('executionId') or req_data.get('execution_id')
from ..api.controller.baseCrudController import BaseCrudController
controller = BaseCrudController(req_data)
try:
if execution_id:
success, msg = JenkinsPollService.poll_jenkins_build_status(controller.session, execution_id)
if not success:
return ApiResponse.build_failure(40012, msg=msg)
execution = AutomationDao.get_execution_by_id(controller.session, execution_id)
return ApiResponse.build_success(20000, data=execution.to_dict() if execution else {'id': execution_id, 'message': msg})
else:
JenkinsPollService.poll_all_pending_executions(controller.session)
return ApiResponse.build_success(20000, data={'message': '轮询完成'})
finally:
controller.close_session()
@api.route('/automation/execution/case/pull', methods=['GET'])
def automation_execution_case_pull():
req_data = dict(request.args)
req_data['_callback_token'] = request.headers.get('X-CALLBACK-TOKEN', '')
controller = AutomationController(req_data)
try:
ret, err_msg = controller.execution_case_pull()
if err_msg:
return ApiResponse.build_failure(40011, msg=err_msg)
return ApiResponse.build_success(20000, data=ret)
finally:
controller.close_session()
@api.route('/automation/execution/queued', methods=['POST'])
def automation_execution_queued():
req_data = request.get_json() or {}
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
controller = AutomationController(req_data)
try:
ok, err_msg = controller.validate_callback_secret()
if not ok:
return ApiResponse.build_failure(40004, msg=err_msg)
update_id, err_msg = controller.execution_queued()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000, data={'id': update_id})
finally:
controller.close_session()
@api.route('/automation/execution/start', methods=['POST'])
def automation_execution_start():
req_data = request.get_json() or {}
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
controller = AutomationController(req_data)
try:
ok, err_msg = controller.validate_callback_secret()
if not ok:
return ApiResponse.build_failure(40004, msg=err_msg)
update_id, err_msg = controller.execution_start()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000, data={'id': update_id})
finally:
controller.close_session()
@api.route('/automation/execution/case/result', methods=['POST'])
def automation_execution_case_result():
req_data = request.get_json() or {}
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
controller = AutomationController(req_data)
try:
ok, err_msg = controller.validate_callback_secret()
if not ok:
return ApiResponse.build_failure(40004, msg=err_msg)
update_id, err_msg = controller.execution_case_result()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000, data={'id': update_id})
finally:
controller.close_session()
@api.route('/automation/execution/finish', methods=['POST'])
def automation_execution_finish():
req_data = request.get_json() or {}
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
controller = AutomationController(req_data)
try:
ok, err_msg = controller.validate_callback_secret()
if not ok:
return ApiResponse.build_failure(40004, msg=err_msg)
update_id, err_msg = controller.execution_finish()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000, data={'id': update_id})
finally:
controller.close_session()
@api.route('/automation/execution/abort', methods=['POST'])
def automation_execution_abort():
req_data = request.get_json() or {}
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
controller = AutomationController(req_data)
try:
ok, err_msg = controller.validate_callback_secret()
if not ok:
return ApiResponse.build_failure(40004, msg=err_msg)
update_id, err_msg = controller.execution_abort()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000, data={'id': update_id})
finally:
controller.close_session()
# =========================
# 报告接口
# =========================
@@ -1250,6 +1444,43 @@ def auth_login():
controller.close_session()
@api.route('/auth/refresh', methods=['POST'])
def auth_refresh():
from .utils.authMiddleware import validate_refresh_token, create_token, create_refresh_token, revoke_refresh_token, get_current_user_id
req_json = request.get_json() or {}
refresh_token = req_json.get('refreshToken') or req_json.get('refresh_token')
access_token = req_json.get('accessToken') or req_json.get('access_token')
if refresh_token:
user_id = validate_refresh_token(refresh_token)
if user_id:
revoke_refresh_token(refresh_token)
new_token, expire_seconds = create_token(user_id)
new_refresh_token, refresh_expire_seconds = create_refresh_token(user_id)
return ApiResponse.build_success(20000, data={
'token': new_token,
'token_type': 'Bearer',
'expires_in': expire_seconds,
'refresh_token': new_refresh_token,
'refresh_expires_in': refresh_expire_seconds
})
return ApiResponse.build_failure(40001, msg='refresh_token无效或已过期')
elif access_token:
user_id = get_current_user_id(access_token)
if user_id:
new_token, expire_seconds = create_token(user_id)
return ApiResponse.build_success(20000, data={
'token': new_token,
'token_type': 'Bearer',
'expires_in': expire_seconds
})
return ApiResponse.build_failure(451, msg='access_token无效或已过期')
return ApiResponse.build_failure(40003, msg='请提供refresh_token或access_token')
@api.route('/bug/list', methods=['GET'])
@login_required
@permission_required('bug:list')