增加项目的各个功能

This commit is contained in:
qiaoxinjiu
2026-05-07 19:21:19 +08:00
parent aba1618f89
commit ee6cd4ae66
121 changed files with 9346 additions and 43 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,57 @@
# encoding: UTF-8
from datetime import date, datetime
from decimal import Decimal
from common.sqlSession import SqlSession
class BaseCrudController(object):
"""通用 Controller 基类,封装公共的请求取值与序列化逻辑。"""
def __init__(self, req_data):
# 每个 controller 持有一个独立 session沿用当前项目的使用方式。
self.session = SqlSession()
# req_data 兼容 request.args 和 request.get_json() 两种来源。
self.req_data = req_data
def close_session(self):
if self.session:
self.session.close()
@staticmethod
def _get(req_data, *keys, default=None):
"""按顺序读取多个候选参数名,兼容前后端字段别名。"""
for key in keys:
value = req_data.get(key)
if value not in (None, ''):
return value
return default
@staticmethod
def _format_value(value):
"""将数据库对象中的特殊类型转成可直接返回给前端的值。"""
if isinstance(value, datetime):
return value.strftime('%Y-%m-%d %H:%M:%S')
if isinstance(value, date):
return value.strftime('%Y-%m-%d')
if isinstance(value, Decimal):
return float(value)
return value
@classmethod
def serialize(cls, item, exclude=None):
"""单对象序列化,可按需排除不希望暴露给前端的字段。"""
if not item:
return {}
exclude = exclude or []
item_dict = item.to_dict()
for key in exclude:
item_dict.pop(key, None)
for key, value in item_dict.items():
item_dict[key] = cls._format_value(value)
return item_dict
@classmethod
def serialize_list(cls, items, exclude=None):
"""列表对象序列化。"""
return [cls.serialize(item, exclude) for item in items]

View File

@@ -0,0 +1,325 @@
# encoding: UTF-8
import os
import uuid
from datetime import datetime
from flask import current_app
from .baseCrudController import BaseCrudController
from ..model.bugModel import Bug, BugComment
from ..model.productModel import Product
from ..model.projectModel import Project
from ..model.userModel import User
from ..model.caseModel import Module
from ..service.bugService import BugService
from ..service.userService import UserService
class BugUploadController(BaseCrudController):
UPLOAD_FOLDER = 'attachment/bug_picture'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
def allowed_file(self, filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in self.ALLOWED_EXTENSIONS
def bug_upload(self):
if 'file' not in self.req_data.files:
return '', '未找到上传文件'
file = self.req_data.files['file']
if file.filename == '':
return '', '文件名不能为空'
if not self.allowed_file(file.filename):
return '', '不支持的文件格式仅支持png, jpg, jpeg, gif, bmp'
try:
os.makedirs(self.UPLOAD_FOLDER, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
ext = file.filename.rsplit('.', 1)[1].lower()
new_filename = f'bug-{timestamp}-{uuid.uuid4().hex[:8]}.{ext}'
file_path = os.path.join(self.UPLOAD_FOLDER, new_filename)
file.save(file_path)
file_url = f'/uploads/{new_filename}'
return file_url, ''
except Exception as e:
return '', f'文件上传失败:{str(e)}'
class BugController(BaseCrudController):
def bug_list(self):
filters = []
product_id = self._get(self.req_data, 'productId', 'product_id')
project_id = self._get(self.req_data, 'projectId', 'project_id')
module_id = self._get(self.req_data, 'moduleId', 'module_id')
bug_type = self._get(self.req_data, 'bugType', 'bug_type')
severity = self._get(self.req_data, 'severity')
priority = self._get(self.req_data, 'priority')
status = self._get(self.req_data, 'status')
assignee_id = self._get(self.req_data, 'assigneeId', 'assignee_id')
reporter_id = self._get(self.req_data, 'reporterId', 'reporter_id')
resolved_by = self._get(self.req_data, 'resolvedBy', 'resolved_by')
reproduce_rate = self._get(self.req_data, 'reproduceRate', 'reproduce_rate')
keyword = self._get(self.req_data, 'keyword')
if product_id:
filters.append(Bug.product_id == int(product_id))
if project_id:
filters.append(Bug.project_id == int(project_id))
if module_id:
filters.append(Bug.module_id == int(module_id))
if bug_type not in (None, ''):
filters.append(Bug.bug_type == int(bug_type))
if severity not in (None, ''):
filters.append(Bug.severity == int(severity))
if priority not in (None, ''):
filters.append(Bug.priority == int(priority))
if status not in (None, ''):
filters.append(Bug.status == int(status))
if assignee_id:
filters.append(Bug.assignee_id == int(assignee_id))
if reporter_id:
filters.append(Bug.reporter_id == int(reporter_id))
if resolved_by:
filters.append(Bug.resolved_by == int(resolved_by))
if reproduce_rate not in (None, ''):
filters.append(Bug.reproduce_rate == int(reproduce_rate))
if keyword:
filters.append(Bug.title.like(f'%{keyword}%') | Bug.description.like(f'%{keyword}%'))
items, total = BugService.list_by_filters(
self.session, Bug, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
Bug.created_time
)
user_ids = []
for item in items:
if item.assignee_id:
user_ids.append(item.assignee_id)
if item.reporter_id:
user_ids.append(item.reporter_id)
if item.resolved_by:
user_ids.append(item.resolved_by)
user_info_map = UserService.get_user_info_map(self.session, user_ids) if user_ids else {}
result_list = []
for item in items:
bug_dict = item.to_dict()
if item.assignee_id and item.assignee_id in user_info_map:
bug_dict['assignee_name'] = user_info_map[item.assignee_id].get('real_name', '')
else:
bug_dict['assignee_name'] = ''
if item.reporter_id and item.reporter_id in user_info_map:
bug_dict['reporter_name'] = user_info_map[item.reporter_id].get('real_name', '')
else:
bug_dict['reporter_name'] = ''
if item.resolved_by and item.resolved_by in user_info_map:
bug_dict['resolved_by_name'] = user_info_map[item.resolved_by].get('real_name', '')
else:
bug_dict['resolved_by_name'] = ''
result_list.append(bug_dict)
return {'list': result_list, 'total': total}
def bug_detail(self):
bug_id = self._get(self.req_data, 'bugId', 'id')
if not bug_id:
return {}, 'bugId 为必传参数'
item = BugService.get_by_id(self.session, Bug, bug_id)
if not item:
return {}, '未查询到对应 Bug'
ret = self.serialize(item, ['is_delete'])
if item.product_id:
product = self.session.query(Product).filter(Product.id == item.product_id, Product.is_delete == 0).first()
ret['product_name'] = product.name if product else ''
if item.project_id:
project = self.session.query(Project).filter(Project.id == item.project_id, Project.is_delete == 0).first()
ret['project_name'] = project.name if project else ''
if item.reporter_id:
reporter = self.session.query(User).filter(User.id == item.reporter_id, User.is_delete == 0).first()
ret['reporter_name'] = reporter.real_name if reporter else ''
if item.assignee_id:
assignee = self.session.query(User).filter(User.id == item.assignee_id, User.is_delete == 0).first()
ret['assignee_name'] = assignee.real_name if assignee else ''
if item.module_id:
module = self.session.query(Module).filter(Module.id == item.module_id, Module.is_delete == 0).first()
ret['module_name'] = module.name if module else ''
if item.resolved_by:
resolved_by_user = self.session.query(User).filter(User.id == item.resolved_by, User.is_delete == 0).first()
ret['resolved_by_name'] = resolved_by_user.real_name if resolved_by_user else ''
comments = BugService.get_comments(self.session, bug_id)
comment_user_ids = [c.user_id for c in comments if c.user_id]
user_info_map = UserService.get_user_info_map(self.session, comment_user_ids) if comment_user_ids else {}
serialized_comments = []
for comment in comments:
comment_dict = comment.to_dict()
if comment.user_id and comment.user_id in user_info_map:
comment_dict['user_name'] = user_info_map[comment.user_id].get('real_name', '')
else:
comment_dict['user_name'] = ''
serialized_comments.append(comment_dict)
ret['comments'] = serialized_comments
history_items = BugService.get_history(self.session, bug_id)
user_ids = set()
for h in history_items:
if h.operator_id:
user_ids.add(h.operator_id)
if h.field_name in ('assignee_id', 'reporter_id', 'user_id', 'resolved_by'):
if h.old_value:
try:
user_ids.add(int(h.old_value))
except (ValueError, TypeError):
pass
if h.new_value:
try:
user_ids.add(int(h.new_value))
except (ValueError, TypeError):
pass
user_info_map = UserService.get_user_info_map(self.session, list(user_ids)) if user_ids else {}
serialized_history = []
for h in history_items:
h_dict = h.to_dict()
if h.operator_id:
h_dict['operator_id'] = user_info_map.get(h.operator_id, {}).get('real_name', h.operator_id)
if h.field_name in ('assignee_id', 'reporter_id', 'user_id', 'resolved_by'):
if h.old_value:
try:
old_uid = int(h.old_value)
h_dict['old_value'] = user_info_map.get(old_uid, {}).get('real_name', h.old_value)
except (ValueError, TypeError):
pass
if h.new_value:
try:
new_uid = int(h.new_value)
h_dict['new_value'] = user_info_map.get(new_uid, {}).get('real_name', h.new_value)
except (ValueError, TypeError):
pass
serialized_history.append(h_dict)
ret['history'] = serialized_history
return ret, ''
def bug_create(self):
title = self._get(self.req_data, 'title')
product_id = self._get(self.req_data, 'productId', 'product_id')
project_id = self._get(self.req_data, 'projectId', 'project_id')
if not title or not product_id or not project_id:
return 0, 'title、productId、projectId 为必传参数'
bug_key = BugService.generate_bug_key(self.session)
add_info = {
'bug_key': bug_key,
'title': title,
'description': self._get(self.req_data, 'description'),
'bug_type': int(self._get(self.req_data, 'bugType', 'bug_type', default=1)),
'severity': int(self._get(self.req_data, 'severity', default=2)),
'priority': int(self._get(self.req_data, 'priority', default=2)),
'status': 0,
'reporter_id': self._get(self.req_data, 'reporterId', 'reporter_id'),
'assignee_id': self._get(self.req_data, 'assigneeId', 'assignee_id'),
'product_id': product_id,
'project_id': project_id,
'module_id': self._get(self.req_data, 'moduleId', 'module_id'),
'case_id': self._get(self.req_data, 'caseId', 'case_id'),
'plan_id': self._get(self.req_data, 'planId', 'plan_id'),
'environment': self._get(self.req_data, 'environment'),
'steps': self._get(self.req_data, 'steps'),
'solution': self._get(self.req_data, 'solution'),
'resolve_version': self._get(self.req_data, 'resolveVersion', 'resolve_version'),
'resolved_by': self._get(self.req_data, 'resolvedBy', 'resolved_by'),
'reproduce_rate': self._get(self.req_data, 'reproduceRate', 'reproduce_rate'),
'is_delete': 0
}
return BugService.create(self.session, Bug, add_info)
def bug_update(self):
bug_id = self._get(self.req_data, 'bugId', 'id')
if not bug_id:
return 0, 'bugId 为必传参数'
update_info = {}
field_mapping = [
(('title',), 'title'),
(('description',), 'description'),
(('bugType', 'bug_type'), 'bug_type'),
(('severity',), 'severity'),
(('priority',), 'priority'),
(('status',), 'status'),
(('assigneeId', 'assignee_id'), 'assignee_id'),
(('reporterId', 'reporter_id'), 'reporter_id'),
(('moduleId', 'module_id'), 'module_id'),
(('caseId', 'case_id'), 'case_id'),
(('planId', 'plan_id'), 'plan_id'),
(('environment',), 'environment'),
(('steps',), 'steps'),
(('solution',), 'solution'),
(('resolveVersion', 'resolve_version'), 'resolve_version'),
(('resolvedBy', 'resolved_by'), 'resolved_by'),
(('reproduceRate', 'reproduce_rate'), 'reproduce_rate')
]
for req_keys, column_key in field_mapping:
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
result = BugService.update_by_id(self.session, Bug, bug_id, update_info)
comment = self._get(self.req_data, 'comment')
user_id = self._get(self.req_data, 'user_id', 'userId')
if comment and user_id:
BugService.add_comment(self.session, bug_id, comment, user_id)
return result
def bug_delete(self):
bug_id = self._get(self.req_data, 'bugId', 'id')
if not bug_id:
return 0, 'bugId 为必传参数'
return BugService.delete_by_id(self.session, Bug, bug_id)
def bug_history_add(self):
bug_id = self._get(self.req_data, 'bugId', 'id')
field_name = self._get(self.req_data, 'fieldName', 'field_name')
old_value = self._get(self.req_data, 'oldValue', 'old_value')
new_value = self._get(self.req_data, 'newValue', 'new_value')
operator_id = self._get(self.req_data, 'operatorId', 'operator_id', 'user_id', 'userId')
if not bug_id:
return 0, 'bugId 为必传参数'
if not field_name:
return 0, 'fieldName 为必传参数'
if not operator_id:
return 0, 'operatorId 为必传参数'
success = BugService.add_history(self.session, bug_id, field_name, old_value, new_value, operator_id)
return 1 if success else 0, '' if success else '添加历史记录失败'
def bug_comment_add(self):
user_id = self._get(self.req_data, 'user_id', 'reporter_id', 'reporterId')
bug_id = self._get(self.req_data, 'bugId')
content = self._get(self.req_data, 'content')
if not bug_id:
return 0, 'bugId 为必传参数'
if not content:
return 0, 'content 为必传参数'
return BugService.add_comment(self.session, bug_id, content, user_id)
def bug_stats(self):
product_id = self._get(self.req_data, 'productId', 'product_id')
project_id = self._get(self.req_data, 'projectId', 'project_id')
return BugService.get_stats(self.session, product_id, project_id)

View File

@@ -0,0 +1,413 @@
# encoding: UTF-8
import os
import json
from sqlalchemy import and_, or_
from flask import g
from .baseCrudController import BaseCrudController
from ..model.caseModel import CaseReview, CaseSnapshot, Module, TestCase
from ..model.projectModel import Project
from ..model.userModel import User
from ..service.caseService import CaseService
from logger import logger
class CaseController(BaseCrudController):
def module_list(self):
project_id = self._get(self.req_data, 'projectId')
parent_id = self._get(self.req_data, 'parentId')
filters = []
if project_id:
filters.append(Module.project_id == int(project_id))
if parent_id not in (None, ''):
filters.append(Module.parent_id == int(parent_id))
parent_module = Module.__table__.alias('parent')
query = self.session.query(Module, parent_module.c.name.label('parent_name')).\
outerjoin(parent_module, Module.parent_id == parent_module.c.id).\
filter(*filters)
if hasattr(Module, 'is_delete'):
query = query.filter(Module.is_delete == 0)
total = query.count()
page_num = int(self._get(self.req_data, 'pageNo', default=1))
page_size = int(self._get(self.req_data, 'pageSize', default=200))
query = query.order_by(Module.id)
items = query.offset((page_num - 1) * page_size).limit(page_size).all()
result_list = []
for module, parent_name in items:
module_dict = self.serialize(module, ['is_delete'])
module_dict['parent_name'] = parent_name or ''
result_list.append(module_dict)
return {'list': result_list, 'total': total}
def module_create(self):
project_id = self._get(self.req_data, 'projectId')
name = self._get(self.req_data, 'name')
if not project_id or not name:
return 0, 'projectId、name 为必传参数'
add_info = {'project_id': project_id, 'parent_id': int(self._get(self.req_data, 'parentId', default=0)), 'name': name, 'sort_order': int(self._get(self.req_data, 'sortOrder', default=0)), 'path': self._get(self.req_data, 'path'), 'is_delete': 0}
return CaseService.create(self.session, Module, add_info)
def module_update(self):
module_id = self._get(self.req_data, 'moduleId', 'id')
if not module_id:
return 0, 'moduleId 为必传参数'
update_info = {}
for req_key, column_key in [('parentId', 'parent_id'), ('name', 'name'), ('sortOrder', 'sort_order'), ('path', 'path')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return CaseService.update_by_id(self.session, Module, module_id, update_info)
def module_delete(self):
module_id = self._get(self.req_data, 'moduleId', 'id')
if not module_id:
return 0, 'moduleId 为必传参数'
return CaseService.delete_by_id(self.session, Module, module_id)
def case_list(self):
"""分页查询用例列表,支持项目名称、用例标题、优先级、类型、状态、是否自动化、标签过滤。"""
filters = []
project_name = self._get(self.req_data, 'projectName')
if project_name:
filters.append(Project.name.like('%{}%'.format(project_name)))
project_id = self._get(self.req_data, 'projectId')
if project_id:
filters.append(TestCase.project_id == int(project_id))
module_name = self._get(self.req_data, 'moduleName', 'module_name')
if module_name:
filters.append(Module.name.like('%{}%'.format(module_name)))
for req_key, column in [('moduleId', TestCase.module_id), ('priority', TestCase.priority),
('caseType', TestCase.case_type), ('status', TestCase.status),
('isAuto', TestCase.is_auto)]:
value = self._get(self.req_data, req_key)
if value not in (None, ''):
filters.append(column == int(value))
keyword = self._get(self.req_data, 'keyword')
if keyword:
filters.append(TestCase.title.like('%{}%'.format(keyword)))
tag = self._get(self.req_data, 'tag')
if tag:
filters.append(TestCase.tags.any(tag))
created_by_name = self._get(self.req_data, 'createdBy')
if created_by_name:
filters.append(User.real_name.like('%{}%'.format(created_by_name)))
query = self.session.query(TestCase, Project.name.label('project_name'), Module.name.label('module_name'), User.real_name.label('created_by_name')).\
join(Project, TestCase.project_id == Project.id, isouter=True).\
join(Module, TestCase.module_id == Module.id, isouter=True).\
join(User, TestCase.created_by == User.id, isouter=True).\
filter(*filters)
if hasattr(TestCase, 'is_delete'):
query = query.filter(TestCase.is_delete == 0)
if hasattr(Project, 'is_delete'):
query = query.filter(Project.is_delete == 0)
if hasattr(Module, 'is_delete'):
query = query.filter(or_(Module.is_delete == 0, Module.is_delete.is_(None)))
if hasattr(User, 'is_delete'):
query = query.filter(or_(User.is_delete == 0, User.is_delete.is_(None)))
total = query.count()
page_num = int(self._get(self.req_data, 'pageNo', 'page', default=1))
page_size = int(self._get(self.req_data, 'pageSize', 'size', default=20))
query = query.order_by(TestCase.id.desc())
items = query.offset((page_num - 1) * page_size).limit(page_size).all()
result_list = []
for case, project_name, module_name, created_by_name in items:
case_dict = self.serialize(case, ['is_delete'])
case_dict['project_name'] = project_name or ''
case_dict['module_name'] = module_name or ''
case_dict['case_key'] = case_dict.get('case_key', '')
case_dict['created_by_name'] = created_by_name or ''
if not case_dict.get('steps'):
case_dict['steps'] = ''
result_list.append(case_dict)
return {'list': result_list, 'total': total}
def case_detail(self):
case_id = self._get(self.req_data, 'caseId', 'id')
if not case_id:
return {}, 'caseId 为必传参数'
item = CaseService.get_by_id(self.session, TestCase, case_id)
if not item:
return {}, '未查询到对应用例!'
result = self.serialize(item, ['is_delete'])
if not result.get('steps'):
result['steps'] = ''
if item.module_id:
module = self.session.query(Module).filter(Module.id == item.module_id).first()
result['module_name'] = module.name if module else ''
else:
result['module_name'] = ''
return result, ''
def case_create(self):
project_id = self._get(self.req_data, 'projectId')
title = self._get(self.req_data, 'title')
if not project_id or not title:
return 0, 'projectId、title 为必传参数'
steps_value = self._get(self.req_data, 'steps', default='')
if isinstance(steps_value, (list, dict)):
steps_value = ''
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),
'title': title,
'preconditions': self._get(self.req_data, 'preconditions'),
'steps': steps_value,
'expected_results': self._get(self.req_data, 'expectedResults'),
'priority': int(self._get(self.req_data, 'priority', default=2)),
'case_type': int(self._get(self.req_data, 'caseType', default=1)),
'tags': self._get(self.req_data, 'tags', default=[]),
'status': int(self._get(self.req_data, 'status', default=1)),
'is_auto': int(self._get(self.req_data, 'isAuto', default=0)),
'created_by': getattr(g, 'current_user_id', None),
'is_delete': 0
}
return CaseService.create(self.session, TestCase, add_info)
def case_update(self):
"""更新用例内容,只更新请求中传入的字段。"""
case_id = self._get(self.req_data, 'caseId', 'id')
if not case_id:
return 0, 'caseId 为必传参数'
update_info = {}
mapping = [('moduleId', 'module_id'), ('caseKey', 'case_key'), ('title', 'title'), ('preconditions', 'preconditions'), ('expectedResults', 'expected_results'), ('priority', 'priority'), ('caseType', 'case_type'), ('tags', 'tags'), ('status', 'status'), ('isAuto', 'is_auto')]
for req_key, column_key in mapping:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
steps_value = self._get(self.req_data, 'steps')
if steps_value is not None:
if isinstance(steps_value, (list, dict)):
steps_value = ''
update_info['steps'] = steps_value
return CaseService.update_by_id(self.session, TestCase, case_id, update_info)
def case_delete(self):
case_id = self._get(self.req_data, 'caseId', 'id')
if not case_id:
return 0, 'caseId 为必传参数'
return CaseService.delete_by_id(self.session, TestCase, case_id)
def snapshot_create(self):
case_id = self._get(self.req_data, 'caseId')
if not case_id:
return 0, 'caseId 为必传参数'
case_obj = CaseService.get_by_id(self.session, TestCase, case_id)
if not case_obj:
return 0, '未查询到对应用例!'
version = CaseService.next_snapshot_version(self.session, case_id)
snapshot = self.serialize(case_obj, ['is_delete'])
if not snapshot.get('steps'):
snapshot['steps'] = ''
add_info = {'case_id': case_id, 'version': version, 'snapshot': snapshot, 'created_by': self._get(self.req_data, 'createdBy')}
return CaseService.create(self.session, CaseSnapshot, add_info)
def snapshot_list(self):
"""查询指定用例的快照历史。"""
case_id = self._get(self.req_data, 'caseId')
filters = [CaseSnapshot.case_id == int(case_id)] if case_id else []
items, total = CaseService.list_by_filters(self.session, CaseSnapshot, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), CaseSnapshot.created_time)
return {'list': self.serialize_list(items), 'total': total}
def review_create(self):
case_id = self._get(self.req_data, 'caseId')
reviewer_id = self._get(self.req_data, 'reviewerId')
if not case_id or not reviewer_id:
return 0, 'caseId、reviewerId 为必传参数'
return CaseService.create(self.session, CaseReview, {'case_id': case_id, 'reviewer_id': reviewer_id, 'comments': self._get(self.req_data, 'comments')})
def review_update(self):
review_id = self._get(self.req_data, 'reviewId', 'id')
if not review_id:
return 0, 'reviewId 为必传参数'
update_info = {}
for req_key, column_key in [('status', 'status'), ('comments', 'comments'), ('diffContent', 'diff_content'), ('reviewedTime', 'reviewed_time')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return CaseService.update_by_id(self.session, CaseReview, review_id, update_info, soft_delete=False)
def review_list(self):
"""查询用例评审记录列表。"""
case_id = self._get(self.req_data, 'caseId')
filters = [CaseReview.case_id == int(case_id)] if case_id else []
items, total = CaseService.list_by_filters(self.session, CaseReview, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), CaseReview.created_time)
return {'list': self.serialize_list(items), 'total': total}
def case_import(self, file_path, project_id):
"""批量导入用例"""
try:
from openpyxl import load_workbook
except ImportError:
return 0, '请先安装 openpyxl 依赖'
if not os.path.exists(file_path):
return 0, '文件不存在'
if not project_id:
return 0, 'projectId 为必传参数'
wb = load_workbook(file_path)
sheet = wb.active
headers = {}
for col in range(1, sheet.max_column + 1):
header = str(sheet.cell(row=1, column=col).value).strip() if sheet.cell(row=1, column=col).value else ''
if header:
headers[header] = col
required_columns = ['所属模块', '用例标题', '前置条件', '步骤', '预期', '关键词', '优先级', '用例类型']
for col in required_columns:
if col not in headers:
return 0, f'缺少必要列: {col}'
module_name_to_id = {}
existing_modules = self.session.query(Module).filter(Module.project_id == int(project_id), Module.is_delete == 0).all()
for module in existing_modules:
module_name_to_id[module.name] = module.id
success_count = 0
fail_count = 0
fail_messages = []
for row in range(2, sheet.max_row + 1):
try:
module_path_str = str(sheet.cell(row=row, column=headers['所属模块']).value).strip() if sheet.cell(row=row, column=headers['所属模块']).value else ''
if not module_path_str:
fail_count += 1
fail_messages.append(f'{row}行:所属模块为空')
continue
module_names = [m.strip() for m in module_path_str.split('/') if m.strip()]
if not module_names:
fail_count += 1
fail_messages.append(f'{row}行:所属模块格式不正确')
continue
parent_id = 0
module_id = None
for idx, module_name in enumerate(module_names):
if module_name in module_name_to_id:
parent_id = module_name_to_id[module_name]
else:
path_parts = module_names[:idx+1]
module_path = '/' + '/'.join(path_parts)
new_module = Module(
project_id=int(project_id),
parent_id=parent_id,
name=module_name,
path=module_path,
is_delete=0
)
self.session.add(new_module)
self.session.flush()
module_name_to_id[module_name] = new_module.id
parent_id = new_module.id
module_id = parent_id
title = str(sheet.cell(row=row, column=headers['用例标题']).value).strip() if sheet.cell(row=row, column=headers['用例标题']).value else ''
preconditions = str(sheet.cell(row=row, column=headers['前置条件']).value).strip() if sheet.cell(row=row, column=headers['前置条件']).value else ''
steps = str(sheet.cell(row=row, column=headers['步骤']).value).strip() if sheet.cell(row=row, column=headers['步骤']).value else ''
expected_results = str(sheet.cell(row=row, column=headers['预期']).value).strip() if sheet.cell(row=row, column=headers['预期']).value else ''
keywords = str(sheet.cell(row=row, column=headers['关键词']).value).strip() if sheet.cell(row=row, column=headers['关键词']).value else ''
priority_str = str(sheet.cell(row=row, column=headers['优先级']).value).strip() if sheet.cell(row=row, column=headers['优先级']).value else '2'
case_type_str = str(sheet.cell(row=row, column=headers['用例类型']).value).strip() if sheet.cell(row=row, column=headers['用例类型']).value else '1'
if not title:
fail_count += 1
fail_messages.append(f'{row}行:用例标题为空')
continue
priority_map = {'P0': 0, 'P1': 1, 'P2': 2, 'P3': 3}
priority = priority_map.get(priority_str, int(priority_str) if priority_str.isdigit() else 2)
case_type_map = {'功能': 1, '性能': 2, '安全': 3, '接口': 4}
case_type = case_type_map.get(case_type_str, int(case_type_str) if case_type_str.isdigit() else 1)
tags = [k.strip() for k in keywords.split(',')] if keywords else []
retry_count = 0
max_retries = 5
case_key = CaseService.next_case_key(self.session, project_id)
while retry_count < max_retries:
try:
case = TestCase(
project_id=int(project_id),
module_id=module_id,
case_key=case_key,
title=title,
preconditions=preconditions,
steps=steps,
expected_results=expected_results,
priority=priority,
case_type=case_type,
tags=tags,
status=1,
is_auto=0,
created_by=getattr(g, 'current_user_id', None),
is_delete=0
)
self.session.add(case)
self.session.flush()
success_count += 1
break
except Exception as e:
if 'duplicate key' in str(e).lower() or 'already exists' in str(e).lower():
logger.warning(f'case_import case_key冲突重新生成{case_key}, 错误:{str(e)}')
case_key = CaseService.next_case_key(self.session, project_id)
retry_count += 1
else:
raise
if retry_count >= max_retries:
fail_count += 1
fail_messages.append(f'{row}行:用例标题[{title}]导入失败case_key生成失败')
except Exception as e:
fail_count += 1
fail_messages.append(f'{row}行:导入失败 - {str(e)}')
try:
self.session.commit()
msg = f'导入完成:成功{success_count}条,失败{fail_count}'
if fail_messages:
msg += f'。失败详情:{"; ".join(fail_messages[:10])}'
if len(fail_messages) > 10:
msg += f'...(共{len(fail_messages)}条)'
return success_count, msg
except Exception as e:
self.session.rollback()
return 0, f'提交失败:{str(e)}'
@staticmethod
def get_template_path():
"""获取模板文件路径"""
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))), 'attachment', '用例导入模版.xlsx')

View File

@@ -0,0 +1,80 @@
# encoding: UTF-8
from .baseCrudController import BaseCrudController
from ..model.dataBuilderModel import DataBuilder, DataTask
from ..service.dataBuilderService import DataBuilderService
class DataBuilderController(BaseCrudController):
"""造数器与造数任务相关接口控制器。"""
def builder_list(self):
"""分页查询造数器列表,可按项目过滤。"""
filters = []
project_id = self._get(self.req_data, 'projectId')
if project_id:
filters.append(DataBuilder.project_id == int(project_id))
items, total = DataBuilderService.list_by_filters(self.session, DataBuilder, filters,
self._get(self.req_data, 'pageNo', default=1),
self._get(self.req_data, 'pageSize', default=20),
DataBuilder.created_time)
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
def builder_detail(self):
"""查询造数器详情。"""
builder_id = self._get(self.req_data, 'builderId', 'id')
if not builder_id:
return {}, 'builderId 为必传参数'
item = DataBuilderService.get_by_id(self.session, DataBuilder, builder_id)
if not item:
return {}, '未查询到对应造数器!'
return self.serialize(item, ['is_delete']), ''
def builder_create(self):
"""创建造数器definition 保存流程编排或模板定义。"""
project_id = self._get(self.req_data, 'projectId')
name = self._get(self.req_data, 'name')
definition = self._get(self.req_data, 'definition')
if not project_id or not name or definition is None:
return 0, 'projectId、name、definition 为必传参数'
add_info = {'project_id': project_id, 'name': name, 'description': self._get(self.req_data, 'description'),
'builder_type': int(self._get(self.req_data, 'builderType', default=1)), 'definition': definition,
'input_schema': self._get(self.req_data, 'inputSchema'),
'output_example': self._get(self.req_data, 'outputExample'),
'created_by': self._get(self.req_data, 'createdBy'), 'is_delete': 0}
return DataBuilderService.create(self.session, DataBuilder, add_info)
def builder_update(self):
builder_id = self._get(self.req_data, 'builderId', 'id')
if not builder_id:
return 0, 'builderId 为必传参数'
update_info = {}
for req_key, column_key in [('name', 'name'), ('description', 'description'), ('builderType', 'builder_type'),
('definition', 'definition'), ('inputSchema', 'input_schema'),
('outputExample', 'output_example')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return DataBuilderService.update_by_id(self.session, DataBuilder, builder_id, update_info)
def builder_delete(self):
builder_id = self._get(self.req_data, 'builderId', 'id')
if not builder_id:
return 0, 'builderId 为必传参数'
return DataBuilderService.delete_by_id(self.session, DataBuilder, builder_id)
def builder_execute(self):
builder_id = self._get(self.req_data, 'builderId')
if not builder_id:
return {}, 'builderId 为必传参数'
return DataBuilderService.execute_builder(self.session, builder_id,
self._get(self.req_data, 'params', default={}),
self._get(self.req_data, 'createdBy'))
def task_status(self):
task_id = self._get(self.req_data, 'taskId')
if not task_id:
return {}, 'taskId 为必传参数'
item = DataBuilderService.get_by_id(self.session, DataTask, task_id, soft_delete=False)
if not item:
return {}, '未查询到对应任务!'
return self.serialize(item), ''

View File

@@ -0,0 +1,192 @@
# encoding: UTF-8
from datetime import datetime
from .baseCrudController import BaseCrudController
from ..model.planModel import PlanCase, TestPlan, TestRound
from ..model.caseModel import Module, TestCase
from ..service.planService import PlanService
from ..service.userService import UserService
class PlanController(BaseCrudController):
def plan_list(self):
filters = []
project_id = self._get(self.req_data, 'projectId', 'project_id')
status = self._get(self.req_data, 'status')
keyword = self._get(self.req_data, 'keyword')
owner_id = self._get(self.req_data, 'ownerId', 'owner_id', 'owner')
if project_id:
filters.append(TestPlan.project_id == int(project_id))
if status not in (None, ''):
filters.append(TestPlan.status == int(status))
if keyword:
filters.append(TestPlan.name.like('%{}%'.format(keyword)))
if owner_id:
filters.append(TestPlan.owner_id == int(owner_id))
items, total = PlanService.list_by_filters(self.session, TestPlan, filters, self._get(self.req_data, 'pageNo', 'page', default=1), self._get(self.req_data, 'pageSize', 'size', default=20), TestPlan.created_time)
owner_ids = [item.owner_id for item in items if item.owner_id]
user_info_map = UserService.get_user_info_map(self.session, owner_ids) if owner_ids else {}
result_list = []
for item in items:
plan_dict = item.to_dict()
if item.owner_id and item.owner_id in user_info_map:
plan_dict['owner_name'] = user_info_map[item.owner_id].get('real_name', '')
else:
plan_dict['owner_name'] = ''
result_list.append(plan_dict)
return {'list': result_list, 'total': total}
def plan_detail(self):
plan_id = self._get(self.req_data, 'planId', 'id')
if not plan_id:
return {}, 'planId 为必传参数'
item = PlanService.get_by_id(self.session, TestPlan, plan_id)
if not item:
return {}, '未查询到对应计划!'
ret = self.serialize(item, ['is_delete'])
ret.update(PlanService.plan_stats(self.session, plan_id))
return ret, ''
def plan_create(self):
project_id = self._get(self.req_data, 'projectId', 'project_id')
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}
return PlanService.create(self.session, TestPlan, add_info)
def plan_update(self):
"""更新测试计划,只更新请求中传入的字段。"""
plan_id = self._get(self.req_data, 'planId', 'id')
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')]:
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
return PlanService.update_by_id(self.session, TestPlan, plan_id, update_info)
def plan_delete(self):
plan_id = self._get(self.req_data, 'planId', 'id')
if not plan_id:
return 0, 'planId 为必传参数'
return PlanService.delete_by_id(self.session, TestPlan, plan_id)
def round_create(self):
plan_id = self._get(self.req_data, 'planId')
round_no = self._get(self.req_data, 'roundNo')
if not plan_id or not round_no:
return 0, 'planId、roundNo 为必传参数'
return PlanService.create(self.session, TestRound, {'plan_id': plan_id, 'round_no': round_no, 'name': self._get(self.req_data, 'name'), 'start_date': self._get(self.req_data, 'startDate'), 'end_date': self._get(self.req_data, 'endDate')})
def round_list(self):
plan_id = self._get(self.req_data, 'planId')
filters = [TestRound.plan_id == int(plan_id)] if plan_id else []
items, total = PlanService.list_by_filters(self.session, TestRound, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=50), TestRound.id)
return {'list': self.serialize_list(items), 'total': total}
def plan_case_add(self):
plan_id = self._get(self.req_data, 'planId')
case_ids = self._get(self.req_data, 'caseIds', default=[])
if not plan_id or not case_ids:
return 0, 'planId、caseIds 为必传参数'
batch_info_list = [{'plan_id': plan_id, 'case_id': case_id, 'assignee_id': self._get(self.req_data, 'assigneeId'), 'round_no': int(self._get(self.req_data, 'roundNo', default=1)), 'status': 0} for case_id in case_ids]
return PlanService.batch_create(self.session, PlanCase, batch_info_list)
def plan_case_list(self):
plan_id = self._get(self.req_data, 'planId')
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, ''):
filters.append(PlanCase.round_no == int(round_no))
items, total = PlanService.list_by_filters(self.session, PlanCase, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), PlanCase.id, asc=True)
case_ids = [item.case_id for item in items if item.case_id]
case_info_map = {}
module_info_map = {}
if case_ids:
cases = self.session.query(TestCase).filter(TestCase.id.in_(case_ids), TestCase.is_delete == 0).all()
case_info_map = {case.id: {'case_key': case.case_key, 'title': case.title, 'module_id': case.module_id} for case in cases}
module_ids = [case.module_id for case in cases if case.module_id]
if module_ids:
modules = self.session.query(Module).filter(Module.id.in_(module_ids), Module.is_delete == 0).all()
module_info_map = {module.id: module.name for module in modules}
result_list = []
for item in items:
case_dict = item.to_dict()
if item.case_id and item.case_id in case_info_map:
case_dict['case_key'] = case_info_map[item.case_id]['case_key']
case_dict['case_title'] = case_info_map[item.case_id]['title']
module_id = case_info_map[item.case_id].get('module_id')
if module_id and module_id in module_info_map:
case_dict['module_name'] = module_info_map[module_id]
else:
case_dict['module_name'] = ''
else:
case_dict['case_key'] = ''
case_dict['case_title'] = ''
case_dict['module_name'] = ''
result_list.append(case_dict)
return {'list': result_list, 'total': total}
def plan_case_execute(self):
plan_case_id = self._get(self.req_data, 'planCaseId', 'id')
if not plan_case_id:
return 0, 'planCaseId 为必传参数'
plan_case = PlanService.get_by_id(self.session, PlanCase, plan_case_id, soft_delete=False)
if not plan_case:
return 0, '未查询到对应计划用例!'
plan_id = plan_case.plan_id
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)
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):
"""查询计划进度统计。"""
plan_id = self._get(self.req_data, 'planId', 'plan_id')
if not plan_id:
return {}, 'planId 为必传参数'
return PlanService.plan_stats(self.session, plan_id), ''

View File

@@ -0,0 +1,64 @@
# encoding: UTF-8
import random
from .baseCrudController import BaseCrudController
from ..model.productModel import Product
from ..service.productService import ProductService
class ProductController(BaseCrudController):
"""产品相关接口控制器。"""
def product_list(self):
filters = []
keyword = self._get(self.req_data, 'keyword')
status = self._get(self.req_data, 'status')
if keyword:
filters.append(Product.name.like('%{}%'.format(keyword)))
if status not in (None, ''):
filters.append(Product.status == int(status))
items, total = ProductService.list_by_filters(self.session, Product, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
Product.created_time)
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
def product_detail(self):
product_id = self._get(self.req_data, 'productId', 'id')
if not product_id:
return {}, 'productId 为必传参数'
item = ProductService.get_by_id(self.session, Product, product_id)
if not item:
return {}, '未查询到对应产品!'
return self.serialize(item, ['is_delete']), ''
def product_create(self):
name = self._get(self.req_data, 'name')
if not name:
return 0, 'name 为必传参数'
add_info = {
'name': name,
'code': str(random.randint(100000, 999999)),
'description': self._get(self.req_data, 'description'),
'status': int(self._get(self.req_data, 'status', default=1)),
'is_delete': 0
}
return ProductService.create(self.session, Product, add_info)
def product_update(self):
product_id = self._get(self.req_data, 'productId', 'id')
if not product_id:
return 0, 'productId 为必传参数'
update_info = {}
for req_key, column_key in [('name', 'name'), ('code', 'code'), ('description', 'description'),
('status', 'status')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return ProductService.update_by_id(self.session, Product, product_id, update_info)
def product_delete(self):
product_id = self._get(self.req_data, 'productId', 'id')
if not product_id:
return 0, 'productId 为必传参数'
return ProductService.delete_by_id(self.session, Product, product_id)

View File

@@ -0,0 +1,198 @@
# encoding: UTF-8
import random
import string
from .baseCrudController import BaseCrudController
from ..model.projectModel import Environment, Project, ProjectMember
from ..service.projectService import ProjectService
from ..service.userService import UserService
from ..dao.rbacDao import RbacDao
class ProjectController(BaseCrudController):
"""项目、项目成员、环境配置相关接口控制器。"""
def project_list(self):
"""分页查询项目列表。"""
page_num = self._get(self.req_data, 'pageNo', 'page', default=1)
page_size = self._get(self.req_data, 'pageSize', 'size', default=20)
keyword = self._get(self.req_data, 'keyword')
status = self._get(self.req_data, 'status')
filter_list = []
# 关键字先按项目名称模糊匹配。
if keyword:
filter_list.append(Project.name.like('%{}%'.format(keyword)))
# 状态字段是枚举数字,查询时显式转 int。
if status not in (None, ''):
filter_list.append(Project.status == int(status))
items, total = ProjectService.list_by_filters(self.session, Project, filter_list, page_num, page_size,
Project.created_time)
product_ids = list({item.product_id for item in items if item.product_id})
product_map = ProjectService.get_product_map(self.session, product_ids)
result_list = self.serialize_list(items, ['is_delete'])
for item in result_list:
item['product_name'] = product_map.get(item.get('product_id'), '')
return {'list': result_list, 'total': total}
def project_detail(self):
"""查询项目详情。"""
project_id = self._get(self.req_data, 'projectId', 'id')
if not project_id:
return {}, 'projectId 为必传参数'
item = ProjectService.get_by_id(self.session, Project, project_id)
if not item:
return {}, '未查询到对应项目!'
return self.serialize(item, ['is_delete']), ''
def project_create(self):
"""创建项目。"""
name = self._get(self.req_data, 'name')
if not name:
return 0, 'name 为必传参数'
add_info = {
'key': ''.join(random.choices(string.ascii_letters + string.digits, k=6)),
'name': name,
'product_id': self._get(self.req_data, 'productId', 'product_id'),
'description': self._get(self.req_data, 'description'),
'department': self._get(self.req_data, 'department'),
# 默认状态为启用。
'status': int(self._get(self.req_data, 'status', default=1)),
'config': self._get(self.req_data, 'config', default={}),
'created_by': self._get(self.req_data, 'createdBy'),
'is_delete': 0
}
return ProjectService.create(self.session, Project, add_info)
def project_update(self):
"""更新项目。"""
project_id = self._get(self.req_data, 'projectId', 'id')
if not project_id:
return 0, 'projectId 为必传参数'
update_info = {}
# 仅更新前端实际传入的字段,避免把未传字段覆盖为空。
for req_key, column_key in [('key', 'key'), ('name', 'name'), ('productId', 'product_id'),
('product_id', 'product_id'), ('description', 'description'),
('department', 'department'), ('status', 'status'), ('config', 'config')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return ProjectService.update_by_id(self.session, Project, project_id, update_info)
def project_delete(self):
"""软删除项目。"""
project_id = self._get(self.req_data, 'projectId', 'id')
if not project_id:
return 0, 'projectId 为必传参数'
return ProjectService.delete_by_id(self.session, Project, project_id)
def environment_list(self):
"""按项目查询环境配置列表。"""
project_id = self._get(self.req_data, 'projectId', 'project_id')
if not project_id:
return {'list': [], 'total': 0}
items, total = ProjectService.list_by_filters(self.session, Environment,
[Environment.project_id == int(project_id)],
self._get(self.req_data, 'pageNo', default=1),
self._get(self.req_data, 'pageSize', default=20),
Environment.created_time)
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
def environment_create(self):
"""新增环境配置。"""
project_id = self._get(self.req_data, 'project_id')
name = self._get(self.req_data, 'name')
variables = self._get(self.req_data, 'variables')
if not project_id or not name or variables is None:
return 0, 'projectId、name、variables 为必传参数'
return ProjectService.create(self.session, Environment, {
'project_id': project_id,
'name': name,
'variables': variables,
# 兼容是否加密开关。
'is_encrypted': bool(self._get(self.req_data, 'isEncrypted', default=False)),
'is_delete': 0
})
def environment_update(self):
"""更新环境配置。"""
env_id = self._get(self.req_data, 'environmentId', 'id')
if not env_id:
return 0, 'environmentId 为必传参数'
update_info = {}
for req_key, column_key in [('name', 'name'), ('variables', 'variables'), ('isEncrypted', 'is_encrypted')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return ProjectService.update_by_id(self.session, Environment, env_id, update_info)
def environment_delete(self):
"""软删除环境配置。"""
env_id = self._get(self.req_data, 'environmentId', 'id')
if not env_id:
return 0, 'environmentId 为必传参数'
return ProjectService.delete_by_id(self.session, Environment, env_id)
def member_list(self):
"""查询项目成员列表(带用户名、角色名称、项目名称)。"""
project_id = self._get(self.req_data, 'projectId', 'project_id')
filters = [ProjectMember.project_id == int(project_id)] if project_id else []
items, total = ProjectService.list_by_filters(self.session, ProjectMember, filters,
self._get(self.req_data, 'pageNo', default=1),
self._get(self.req_data, 'pageSize', default=20),
ProjectMember.joined_time)
result_list = self.serialize_list(items)
if not result_list:
return {'list': result_list, 'total': total}
user_ids = [item.get('user_id') for item in result_list]
project_ids = [item.get('project_id') for item in result_list]
user_map = UserService.get_user_info_map(self.session, user_ids)
project_map = ProjectService.get_project_name_map(self.session, project_ids)
user_role_map = UserService.get_user_roles_map(self.session, user_ids)
for item in result_list:
user_id = item.get('user_id')
user_info = user_map.get(user_id, {})
item['real_name'] = user_info.get('real_name', '')
item['username'] = user_info.get('username', '')
project_info = project_map.get(item.get('project_id'), {})
item['project_name'] = project_info.get('name', '')
role_info = user_role_map.get(user_id, {})
role_names = role_info.get('role_names', [])
item['role_names'] = role_names
item['role_name'] = ','.join(role_names) if role_names else ''
return {'list': result_list, 'total': total}
def member_create(self):
"""批量新增项目成员(根据用户系统角色自动映射项目成员角色)。"""
project_id = self._get(self.req_data, 'project_id')
user_ids = self._get(self.req_data, 'user_ids')
if not project_id or not user_ids:
return 0, 'project_id、user_ids 为必传参数'
if not isinstance(user_ids, list):
return 0, 'user_ids 必须为数组'
if not user_ids:
return 0, 'user_ids 不能为空'
user_role_map = UserService.get_user_roles_map(self.session, user_ids)
role_name_map = RbacDao.get_role_name_map(self.session)
name_to_project_role = {name: role_id for role_id, name in role_name_map.items()}
created_ids = []
for user_id in user_ids:
role_info = user_role_map.get(user_id, {})
role_names = role_info.get('role_names', [])
project_role = 0
for role_name in role_names:
if role_name in name_to_project_role:
project_role = name_to_project_role[role_name]
break
if project_role == 0:
return 0, f'用户 {user_id} 未分配有效角色,无法添加为项目成员'
create_id, err_msg = ProjectService.create(self.session, ProjectMember, {
'project_id': project_id,
'user_id': user_id,
'role': project_role
})
if err_msg:
return 0, f'用户 {user_id} 添加失败:{err_msg}'
created_ids.append(create_id)
return created_ids[0] if len(created_ids) == 1 else created_ids, ''

View File

@@ -0,0 +1,225 @@
# encoding: UTF-8
import time
import hmac
import hashlib
import base64
import requests
from .baseCrudController import BaseCrudController
from ..model.projectHookModel import ProjectHook
from ..service.projectHookService import ProjectHookService
class ProjectHookController(BaseCrudController):
def hook_list(self):
filters = []
project_id = self._get(self.req_data, 'projectId', 'project_id')
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
if project_id:
filters.append(ProjectHook.project_id == int(project_id))
if hook_type not in (None, ''):
filters.append(ProjectHook.hook_type == int(hook_type))
items, total = ProjectHookService.list_by_filters(
self.session, ProjectHook, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
ProjectHook.created_time
)
result_list = []
hook_type_map = {1: '飞书', 2: '钉钉', 3: '企微'}
for item in items:
hook_dict = item.to_dict()
hook_dict['hook_type_name'] = hook_type_map.get(item.hook_type, '')
result_list.append(hook_dict)
return {'list': result_list, 'total': total}
def hook_detail(self):
hook_id = self._get(self.req_data, 'hookId', 'id')
if not hook_id:
return {}, 'hookId 为必传参数'
item = ProjectHookService.get_by_id(self.session, ProjectHook, hook_id)
if not item:
return {}, '未查询到对应Hook配置'
ret = item.to_dict()
hook_type_map = {1: '飞书', 2: '钉钉', 3: '企微'}
ret['hook_type_name'] = hook_type_map.get(item.hook_type, '')
return ret, ''
def hook_create(self):
project_id = self._get(self.req_data, 'projectId', 'project_id')
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
webhook_url = self._get(self.req_data, 'webhookUrl', 'webhook_url')
if not project_id:
return 0, 'projectId 为必传参数'
if not hook_type:
return 0, 'hookType 为必传参数'
if not webhook_url:
return 0, 'webhookUrl 为必传参数'
add_info = {
'project_id': project_id,
'hook_type': int(hook_type),
'webhook_url': webhook_url,
'secret': self._get(self.req_data, 'secret'),
'enabled': int(self._get(self.req_data, 'enabled', default=1)),
'description': self._get(self.req_data, 'description'),
'config': self._get(self.req_data, 'config', default={}),
'is_delete': 0
}
return ProjectHookService.create(self.session, ProjectHook, add_info)
def hook_update(self):
hook_id = self._get(self.req_data, 'hookId', 'id')
if not hook_id:
return 0, 'hookId 为必传参数'
update_info = {}
field_mapping = [
(('hookType', 'hook_type'), 'hook_type'),
(('webhookUrl', 'webhook_url'), 'webhook_url'),
(('secret',), 'secret'),
(('enabled',), 'enabled'),
(('description',), 'description'),
(('config',), 'config')
]
for req_keys, column_key in field_mapping:
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
return ProjectHookService.update_by_id(self.session, ProjectHook, hook_id, update_info)
def hook_delete(self):
hook_id = self._get(self.req_data, 'hookId', 'id')
if not hook_id:
return 0, 'hookId 为必传参数'
return ProjectHookService.delete_by_id(self.session, ProjectHook, hook_id)
def hook_send(self):
project_id = self._get(self.req_data, 'projectId', 'project_id')
title = self._get(self.req_data, 'title')
content = self._get(self.req_data, 'content')
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
hook_id = self._get(self.req_data, 'hookId', 'id')
real_name = self._get(self.req_data, 'real_name', 'realName')
if not project_id:
return 0, 'projectId 为必传参数'
if not title:
return 0, 'title 为必传参数'
if not content:
return 0, 'content 为必传参数'
at_prefix = f'@{real_name} ' if real_name else ''
final_content = f'{at_prefix}{content}'
if hook_id:
hook = ProjectHookService.get_by_id(self.session, ProjectHook, hook_id)
if not hook or hook.is_delete == 1 or hook.enabled != 1:
return 0, '未找到对应的Hook或Hook未启用'
hooks = [hook]
else:
hooks = ProjectHookService.get_hooks_by_project(self.session, project_id, hook_type)
if not hooks:
return 0, '未配置对应的Hook'
results = []
for hook in hooks:
if hook.hook_type == 1:
success, err_msg = self._send_feishu_message(hook.webhook_url, hook.secret, title, final_content)
elif hook.hook_type == 2:
success, err_msg = self._send_dingtalk_message(hook.webhook_url, hook.secret, title, final_content)
elif hook.hook_type == 3:
success, err_msg = self._send_wecom_message(hook.webhook_url, hook.secret, title, final_content)
else:
success, err_msg = False, '未知Hook类型'
results.append({
'hook_id': hook.id,
'hook_type': hook.hook_type,
'success': success,
'error': err_msg
})
all_success = all(r['success'] for r in results)
return 1 if all_success else 0, results
def _send_feishu_message(self, webhook_url, secret, title, content):
timestamp = str(int(time.time()))
sign = ''
if secret:
string_to_sign = f'{timestamp}\n{secret}'
hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256).digest()
sign = base64.b64encode(hmac_code).decode('utf-8')
separator = '&' if '?' in webhook_url else '?'
url = f'{webhook_url}{separator}timestamp={timestamp}&sign={sign}' if sign else webhook_url
payload = {
'msg_type': 'text',
'content': {
'text': f'{title}\n\n{content}'
}
}
try:
response = requests.post(url, json=payload, timeout=10)
result = response.json()
if result.get('code') == 0:
return True, ''
else:
return False, result.get('msg', '发送失败')
except Exception as e:
return False, str(e)
def _send_dingtalk_message(self, webhook_url, secret, title, content):
timestamp = str(int(time.time() * 1000))
sign = ''
if secret:
string_to_sign = f'{timestamp}\n{secret}'
hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256).digest()
sign = base64.b64encode(hmac_code).decode('utf-8')
separator = '&' if '?' in webhook_url else '?'
url = f'{webhook_url}{separator}timestamp={timestamp}&sign={sign}' if sign else webhook_url
payload = {
'msgtype': 'text',
'text': {
'content': f'{title}\n\n{content}'
}
}
try:
response = requests.post(url, json=payload, timeout=10)
result = response.json()
if result.get('errcode') == 0:
return True, ''
else:
return False, result.get('errmsg', '发送失败')
except Exception as e:
return False, str(e)
def _send_wecom_message(self, webhook_url, secret, title, content):
payload = {
'msgtype': 'text',
'text': {
'content': f'{title}\n\n{content}'
}
}
try:
response = requests.post(webhook_url, json=payload, timeout=10)
result = response.json()
if result.get('errcode') == 0:
return True, ''
else:
return False, result.get('errmsg', '发送失败')
except Exception as e:
return False, str(e)

View File

@@ -0,0 +1,257 @@
# encoding: UTF-8
from flask import g
from .baseCrudController import BaseCrudController
from ..model.rbacModel import Role, Permission, Menu, RolePermission
from ..service.rbacService import RbacService
class RbacController(BaseCrudController):
def role_list(self):
filters = []
status = self._get(self.req_data, 'status')
if status not in (None, ''):
filters.append(Menu.status == int(status))
return RbacService.build_menu_tree(
self.session,
filters,
role_ids=getattr(g, 'current_role_ids', [])
)
def role_page_list(self):
filters = []
keyword = self._get(self.req_data, 'keyword')
status = self._get(self.req_data, 'status')
if keyword:
filters.append(Role.name.like('%{}%'.format(keyword)))
if status not in (None, ''):
filters.append(Role.status == int(status))
items, total = RbacService.list_by_filters(self.session, Role, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
Role.created_time)
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
def role_detail(self):
role_id = self._get(self.req_data, 'roleId', 'id')
if not role_id:
return {}, 'roleId 为必传参数'
item = RbacService.get_by_id(self.session, Role, role_id)
if not item:
return {}, '未查询到对应角色!'
return self.serialize(item, ['is_delete']), ''
def role_create(self):
code = self._get(self.req_data, 'code')
name = self._get(self.req_data, 'name')
if not code or not name:
return 0, 'code、name 为必传参数'
return RbacService.create(self.session, Role, {
'code': code,
'name': name,
'description': self._get(self.req_data, 'description'),
'status': int(self._get(self.req_data, 'status', default=1)),
'is_system': int(self._get(self.req_data, 'isSystem', 'is_system', default=0)),
'created_by': self._get(self.req_data, 'createdBy'),
'is_delete': 0
})
def role_update(self):
role_id = self._get(self.req_data, 'roleId', 'id')
if not role_id:
return 0, 'roleId 为必传参数'
update_info = {}
for req_key, column_key in [('code', 'code'), ('name', 'name'), ('description', 'description'),
('status', 'status'), ('isSystem', 'is_system'), ('is_system', 'is_system')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return RbacService.update_by_id(self.session, Role, role_id, update_info)
def role_delete(self):
role_id = self._get(self.req_data, 'roleId', 'id')
if not role_id:
return 0, 'roleId 为必传参数'
return RbacService.delete_by_id(self.session, Role, role_id)
def permission_list(self):
filters = []
keyword = self._get(self.req_data, 'keyword')
module = self._get(self.req_data, 'module')
status = self._get(self.req_data, 'status')
if keyword:
filters.append(Permission.name.like('%{}%'.format(keyword)))
if module:
filters.append(Permission.module == module)
if status not in (None, ''):
filters.append(Permission.status == int(status))
items, total = RbacService.list_by_filters(self.session, Permission, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
Permission.created_time)
role_permission_items = self.session.query(RolePermission).filter(RolePermission.is_delete == 0).all()
permission_role_map = {}
for rp in role_permission_items:
if rp.permission_id not in permission_role_map:
permission_role_map[rp.permission_id] = []
permission_role_map[rp.permission_id].append(rp.role_id)
role_items = self.session.query(Role).filter(Role.is_delete == 0).all()
role_map = {r.id: {'id': r.id, 'name': r.name} for r in role_items}
result_list = []
for item in items:
item_dict = self.serialize(item, ['is_delete'])
role_ids = permission_role_map.get(item.id, [])
item_dict['roles'] = [role_map.get(rid) for rid in role_ids if role_map.get(rid)]
result_list.append(item_dict)
return {'list': result_list, 'total': total}
def permission_detail(self):
permission_id = self._get(self.req_data, 'permissionId', 'id')
if not permission_id:
return {}, 'permissionId 为必传参数'
item = RbacService.get_by_id(self.session, Permission, permission_id)
if not item:
return {}, '未查询到对应权限!'
return self.serialize(item, ['is_delete']), ''
def permission_create(self):
code = self._get(self.req_data, 'code')
name = self._get(self.req_data, 'name')
if not code or not name:
return 0, 'code、name 为必传参数'
return RbacService.create(self.session, Permission, {
'code': code,
'name': name,
'module': self._get(self.req_data, 'module'),
'action': self._get(self.req_data, 'action'),
'description': self._get(self.req_data, 'description'),
'status': int(self._get(self.req_data, 'status', default=1)),
'is_delete': 0
})
def permission_update(self):
permission_id = self._get(self.req_data, 'permissionId', 'id')
if not permission_id:
return 0, 'permissionId 为必传参数'
update_info = {}
for req_key, column_key in [('code', 'code'), ('name', 'name'), ('module', 'module'), ('action', 'action'),
('description', 'description'), ('status', 'status')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return RbacService.update_by_id(self.session, Permission, permission_id, update_info)
def permission_delete(self):
permission_id = self._get(self.req_data, 'permissionId', 'id')
if not permission_id:
return 0, 'permissionId 为必传参数'
return RbacService.delete_by_id(self.session, Permission, permission_id)
def menu_tree(self):
return RbacService.build_menu_tree(self.session, [])
def current_menu_list(self):
filters = []
status = self._get(self.req_data, 'status')
if status not in (None, ''):
filters.append(Menu.status == int(status))
return RbacService.build_menu_tree(
self.session,
filters,
role_ids=getattr(g, 'current_role_ids', [])
)
def role_menu_tree(self):
role_id = self._get(self.req_data, 'roleId')
if not role_id:
return {'tree': [], 'checkedKeys': []}, 'roleId 为必传参数'
return {
'tree': RbacService.build_menu_tree(self.session, []),
'checkedKeys': RbacService.get_role_menu_ids(self.session, role_id)
}, ''
def menu_detail(self):
menu_id = self._get(self.req_data, 'menuId', 'id')
if not menu_id:
return {}, 'menuId 为必传参数'
item = RbacService.get_by_id(self.session, Menu, menu_id)
if not item:
return {}, '未查询到对应菜单!'
return self.serialize(item, ['is_delete']), ''
def menu_create(self):
name = self._get(self.req_data, 'name')
if not name:
return 0, 'name 为必传参数'
return RbacService.create(self.session, Menu, {
'parent_id': int(self._get(self.req_data, 'parentId', 'parent_id', default=0)),
'name': name,
'code': self._get(self.req_data, 'code'),
'type': int(self._get(self.req_data, 'type', default=1)),
'path': self._get(self.req_data, 'path'),
'component': self._get(self.req_data, 'component'),
'icon': self._get(self.req_data, 'icon'),
'permission_code': self._get(self.req_data, 'permissionCode', 'permission_code'),
'sort': int(self._get(self.req_data, 'sort', default=0)),
'visible': int(self._get(self.req_data, 'visible', default=1)),
'status': int(self._get(self.req_data, 'status', default=1)),
'is_delete': 0
})
def menu_update(self):
menu_id = self._get(self.req_data, 'menuId', 'id')
if not menu_id:
return 0, 'menuId 为必传参数'
update_info = {}
field_pairs = [
(('parentId', 'parent_id'), 'parent_id'),
(('name',), 'name'),
(('code',), 'code'),
(('type',), 'type'),
(('path',), 'path'),
(('component',), 'component'),
(('icon',), 'icon'),
(('permissionCode', 'permission_code'), 'permission_code'),
(('sort',), 'sort'),
(('visible',), 'visible'),
(('status',), 'status')
]
for req_keys, column_key in field_pairs:
value = self._get(self.req_data, *req_keys)
if value is not None:
update_info[column_key] = value
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 为必传参数'
return RbacService.delete_by_id(self.session, Menu, menu_id)
def role_permission_list(self):
role_id = self._get(self.req_data, 'roleId')
if not role_id:
return {'permissionIds': []}
return {'permissionIds': RbacService.get_role_permission_ids(self.session, role_id)}
def role_permission_assign(self):
role_ids = self._get(self.req_data, 'roleIds', default=[])
permission_id = self._get(self.req_data, 'permissionId')
if not role_ids:
return 0, 'roleIds 为必传参数'
if not permission_id:
return 0, 'permissionId 为必传参数'
return RbacService.assign_permissions(self.session, role_ids, permission_id)
def role_menu_list(self):
role_id = self._get(self.req_data, 'roleId')
if not role_id:
return {'menuIds': []}
return {'menuIds': RbacService.get_role_menu_ids(self.session, role_id)}
def role_menu_assign(self):
role_id = self._get(self.req_data, 'roleId')
menu_ids = self._get(self.req_data, 'menuIds', default=[])
if not role_id:
return 0, 'roleId 为必传参数'
return RbacService.assign_menus(self.session, role_id, menu_ids)

View File

@@ -0,0 +1,47 @@
# encoding: UTF-8
from .baseCrudController import BaseCrudController
from ..model.planModel import TestPlan
from ..model.reportModel import Report
from ..service.reportService import ReportService
class ReportController(BaseCrudController):
"""测试报告相关接口控制器。"""
def report_list(self):
"""分页查询报告列表,可按产品、项目、计划过滤。"""
filters = []
product_id = self._get(self.req_data, 'productId', 'product_id')
if product_id:
filters.append(Report.product_id == int(product_id))
project_id = self._get(self.req_data, 'projectId', 'project_id')
if project_id:
filters.append(Report.project_id == int(project_id))
plan_id = self._get(self.req_data, 'planId', 'plan_id')
if plan_id:
filters.append(Report.plan_id == int(plan_id))
items, total = ReportService.list_by_filters(self.session, Report, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), Report.generated_time)
result_list = []
for item in items:
item_dict = self.serialize(item)
plan = self.session.query(TestPlan).filter(TestPlan.id == item.plan_id).first()
item_dict['plan_name'] = plan.name if plan else None
result_list.append(item_dict)
return {'list': result_list, 'total': total}
def report_detail(self):
"""查询报告详情,返回 summary 和 HTML content。"""
report_id = self._get(self.req_data, 'reportId', 'report_id', 'id')
if not report_id:
return {}, 'reportId 为必传参数'
item = ReportService.get_by_id(self.session, Report, report_id)
if not item:
return {}, '未查询到对应报告!'
return self.serialize(item), ''
def report_generate(self):
"""同步生成报告:聚合计划执行数据并落库。"""
plan_id = self._get(self.req_data, 'planId', 'plan_id')
if not plan_id:
return 0, 'planId 为必传参数'
return ReportService.generate_report(self.session, plan_id, self._get(self.req_data, 'generatedBy', 'generated_by'))

View File

@@ -0,0 +1,128 @@
# encoding: UTF-8
from .baseCrudController import BaseCrudController
from ..model.userModel import User
from ..service.userService import UserService
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token
class UserController(BaseCrudController):
def user_list(self):
filters = []
keyword = self._get(self.req_data, 'keyword')
status = self._get(self.req_data, 'status')
if keyword:
filters.append(User.username.like('%{}%'.format(keyword)))
if status not in (None, ''):
filters.append(User.status == int(status))
items, total = UserService.list_by_filters(self.session, User, filters,
self._get(self.req_data, 'pageNo', 'page', default=1),
self._get(self.req_data, 'pageSize', 'size', default=20),
User.created_time)
result_list = self.serialize_list(items, ['is_delete', 'password_hash'])
role_map = UserService.get_user_roles_map(self.session, [item.id for item in items])
for item in result_list:
role_info = role_map.get(item.get('id'), {'role_ids': [], 'role_names': []})
item['role_ids'] = role_info['role_ids']
item['role_names'] = role_info['role_names']
return {'list': result_list, 'total': total}
def user_detail(self):
user_id = self._get(self.req_data, 'userId', 'id')
if not user_id:
return {}, 'userId 为必传参数'
item = UserService.get_by_id(self.session, User, user_id)
if not item:
return {}, '未查询到对应用户!'
ret = self.serialize(item, ['is_delete', 'password_hash'])
ret['role_ids'] = UserService.get_user_role_ids(self.session, user_id)
return ret, ''
def user_create(self):
username = self._get(self.req_data, 'username')
password = self._get(self.req_data, 'password')
if not username or not password:
return 0, 'username、password 为必传参数'
return UserService.create(self.session, User, {
'username': username,
'real_name': self._get(self.req_data, 'realName', 'real_name'),
'password_hash': password,
'mobile': self._get(self.req_data, 'mobile'),
'email': self._get(self.req_data, 'email'),
'avatar': self._get(self.req_data, 'avatar'),
'status': int(self._get(self.req_data, 'status', default=1)),
'created_by': self._get(self.req_data, 'createdBy'),
'is_delete': 0
})
def user_update(self):
user_id = self._get(self.req_data, 'userId', 'id')
if not user_id:
return 0, 'userId 为必传参数'
update_info = {}
for req_key, column_key in [('username', 'username'), ('realName', 'real_name'), ('real_name', 'real_name'),
('password', 'password_hash'), ('mobile', 'mobile'), ('email', 'email'),
('avatar', 'avatar'), ('status', 'status')]:
value = self._get(self.req_data, req_key)
if value is not None:
update_info[column_key] = value
return UserService.update_by_id(self.session, User, user_id, update_info)
def user_delete(self):
user_id = self._get(self.req_data, 'userId', 'id')
if not user_id:
return 0, 'userId 为必传参数'
return UserService.delete_by_id(self.session, User, user_id)
def user_role_list(self):
user_id = self._get(self.req_data, 'userId')
if not user_id:
return {'roleIds': []}
return {'roleIds': UserService.get_user_role_ids(self.session, user_id)}
def user_role_assign(self):
user_id = self._get(self.req_data, 'userId')
role_ids = self._get(self.req_data, 'roleIds', default=[])
if not user_id:
return 0, 'userId 为必传参数'
return UserService.assign_roles(self.session, user_id, role_ids)
def register(self):
username = self._get(self.req_data, 'username')
password = self._get(self.req_data, 'password')
if not username or not password:
return 0, 'username、password 为必传参数'
exist_user = UserService.get_by_username(self.session, username)
if exist_user:
return 0, '用户名已存在!'
return UserService.create(self.session, User, {
'username': username,
'real_name': self._get(self.req_data, 'realName', 'real_name'),
'password_hash': password,
'mobile': self._get(self.req_data, 'mobile'),
'email': self._get(self.req_data, 'email'),
'avatar': self._get(self.req_data, 'avatar'),
'status': 1,
'created_by': self._get(self.req_data, 'createdBy'),
'is_delete': 0
})
def login(self):
username = self._get(self.req_data, 'username')
password = self._get(self.req_data, 'password')
if not username or not password:
return {}, 'username、password 为必传参数'
user = UserService.get_by_username(self.session, username)
if not user or user.password_hash != password:
return {}, '用户名或密码错误!'
if int(user.status) != 1:
return {}, '用户已禁用!'
UserService.update_last_login_time(self.session, user.id)
token, expire_seconds = create_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_threshold_seconds'] = TOKEN_REFRESH_THRESHOLD_SECONDS
ret['refresh_mechanism'] = '请求任意已登录接口时若token剩余有效期小于阈值则自动续期到完整有效期'
return ret, ''

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

224
app/api/dao/bugDao.py Normal file
View File

@@ -0,0 +1,224 @@
# encoding: UTF-8
from sqlalchemy import func, cast, Date
from ..model.bugModel import Bug, BugComment, BugHistory
from ..model.userModel import User
from ..model.caseModel import Module
from logger import logger
class BugDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.asc() if asc else order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return BugDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def generate_bug_key(session):
max_key = session.query(func.max(Bug.bug_key)).filter(Bug.bug_key.like('BUG-%')).scalar()
if max_key:
num = int(max_key.split('-')[1]) + 1
else:
num = 1
return f'BUG-{num:03d}'
@staticmethod
def get_comments(session, bug_id):
return session.query(BugComment).filter(
BugComment.bug_id == int(bug_id),
BugComment.is_delete == 0
).order_by(BugComment.created_time.desc()).all()
@staticmethod
def get_history(session, bug_id):
return session.query(BugHistory).filter(
BugHistory.bug_id == int(bug_id)
).order_by(BugHistory.created_time.desc()).all()
@staticmethod
def add_history(session, bug_id, field_name, old_value, new_value, operator_id):
session.add(BugHistory(
bug_id=bug_id,
field_name=field_name,
old_value=str(old_value) if old_value else None,
new_value=str(new_value) if new_value else None,
operator_id=operator_id
))
err = session.done(close=False)
if err:
logger.warning(f'BugHistory新增失败{err}')
return False
return True
@staticmethod
def get_stats(session, product_id=None, project_id=None):
query = session.query(Bug).filter(Bug.is_delete == 0)
if product_id:
query = query.filter(Bug.product_id == int(product_id))
if project_id:
query = query.filter(Bug.project_id == int(project_id))
total = query.count()
new_count = query.filter(Bug.status == 0).count()
pending_count = query.filter(Bug.status == 1).count()
in_progress_count = query.filter(Bug.status == 2).count()
resolved_count = query.filter(Bug.status == 3).count()
closed_count = query.filter(Bug.status == 4).count()
rejected_count = query.filter(Bug.status == 5).count()
by_status = {}
for status in range(6):
by_status[str(status)] = query.filter(Bug.status == status).count()
by_solution = {}
solution_results = session.query(
Bug.solution, func.count(Bug.id)
).filter(Bug.is_delete == 0)
if product_id:
solution_results = solution_results.filter(Bug.product_id == int(product_id))
if project_id:
solution_results = solution_results.filter(Bug.project_id == int(project_id))
solution_results = solution_results.filter(Bug.solution.isnot(None)).group_by(Bug.solution).all()
for solution, count in solution_results:
by_solution[solution] = count
by_reporter = {}
reporter_results = session.query(
User.real_name, func.count(Bug.id)
).join(User, Bug.reporter_id == User.id).filter(Bug.is_delete == 0)
if product_id:
reporter_results = reporter_results.filter(Bug.product_id == int(product_id))
if project_id:
reporter_results = reporter_results.filter(Bug.project_id == int(project_id))
reporter_results = reporter_results.group_by(User.real_name).all()
for name, count in reporter_results:
by_reporter[name] = count
by_assignee = {}
assignee_results = session.query(
User.real_name, func.count(Bug.id)
).outerjoin(User, Bug.assignee_id == User.id).filter(Bug.is_delete == 0)
if product_id:
assignee_results = assignee_results.filter(Bug.product_id == int(product_id))
if project_id:
assignee_results = assignee_results.filter(Bug.project_id == int(project_id))
assignee_results = assignee_results.group_by(User.real_name).all()
for name, count in assignee_results:
by_assignee[name or '未指派'] = count
by_resolver = {}
resolver_results = session.query(
User.real_name, func.count(Bug.id)
).outerjoin(User, Bug.resolved_by == User.id).filter(Bug.is_delete == 0)
if product_id:
resolver_results = resolver_results.filter(Bug.product_id == int(product_id))
if project_id:
resolver_results = resolver_results.filter(Bug.project_id == int(project_id))
resolver_results = resolver_results.group_by(User.real_name).all()
for name, count in resolver_results:
by_resolver[name or '未解决'] = count
by_module = {}
module_results = session.query(
Module.name, func.count(Bug.id)
).outerjoin(Module, Bug.module_id == Module.id).filter(Bug.is_delete == 0)
if product_id:
module_results = module_results.filter(Bug.product_id == int(product_id))
if project_id:
module_results = module_results.filter(Bug.project_id == int(project_id))
module_results = module_results.group_by(Module.name).all()
for name, count in module_results:
by_module[name or '未分类'] = count
by_version = {}
version_results = session.query(
Bug.resolve_version, func.count(Bug.id)
).filter(Bug.is_delete == 0)
if product_id:
version_results = version_results.filter(Bug.product_id == int(product_id))
if project_id:
version_results = version_results.filter(Bug.project_id == int(project_id))
version_results = version_results.filter(Bug.resolve_version.isnot(None)).group_by(Bug.resolve_version).all()
for version, count in version_results:
by_version[version] = count
by_activation = {}
daily_new = {}
daily_new_results = session.query(
cast(Bug.created_time, Date).label('stat_date'),
func.count(Bug.id)
).filter(Bug.is_delete == 0)
if product_id:
daily_new_results = daily_new_results.filter(Bug.product_id == int(product_id))
if project_id:
daily_new_results = daily_new_results.filter(Bug.project_id == int(project_id))
daily_new_results = daily_new_results.group_by('stat_date').order_by('stat_date').all()
for date, count in daily_new_results:
daily_new[str(date)] = count
daily_resolved = {}
daily_closed = {}
return {
'total': total,
'new': new_count,
'pending': pending_count,
'in_progress': in_progress_count,
'resolved': resolved_count,
'closed': closed_count,
'rejected': rejected_count,
'by_status': by_status,
'by_solution': by_solution,
'by_reporter': by_reporter,
'by_assignee': by_assignee,
'by_resolver': by_resolver,
'by_module': by_module,
'by_version': by_version,
'by_activation': by_activation,
'daily_new': daily_new,
'daily_resolved': daily_resolved,
'daily_closed': daily_closed
}

90
app/api/dao/caseDao.py Normal file
View File

@@ -0,0 +1,90 @@
# encoding: UTF-8
from sqlalchemy import func
from ..model.caseModel import CaseReview, CaseSnapshot, Module, TestCase
from logger import logger
class CaseDao(object):
"""用例域通用 DAO复用模块、用例、快照、评审的基础操作。"""
@staticmethod
def create(session, model_cls, add_info):
"""创建记录并提交事务。"""
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
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)
@staticmethod
def next_snapshot_version(session, case_id):
"""生成用例快照版本号。"""
max_version = session.query(func.max(CaseSnapshot.version)).filter(CaseSnapshot.case_id == int(case_id)).scalar() or 0
return int(max_version) + 1
@staticmethod
def module_model():
return Module
@staticmethod
def case_model():
return TestCase
@staticmethod
def snapshot_model():
return CaseSnapshot
@staticmethod
def review_model():
return CaseReview
@staticmethod
def get_module_name_map(session, module_ids):
if not module_ids:
return {}
module_items = session.query(Module).filter(Module.id.in_(module_ids), Module.is_delete == 0).all()
return {module.id: module.name for module in module_items}

View File

@@ -0,0 +1,59 @@
# encoding: UTF-8
from ..model.dataBuilderModel import DataBuilder, DataTask
from logger import logger
class DataBuilderDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return DataBuilderDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def builder_model():
return DataBuilder
@staticmethod
def task_model():
return DataTask

88
app/api/dao/planDao.py Normal file
View File

@@ -0,0 +1,88 @@
# encoding: UTF-8
from sqlalchemy import func
from ..model.planModel import PlanCase, TestPlan, TestRound
from logger import logger
class PlanDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def batch_create(session, model_cls, batch_info_list):
if not batch_info_list:
return 0, ''
objs = [model_cls(**info) for info in batch_info_list]
session.add_all(objs)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}批量新增失败!{err}')
return 0, f'批量新增失败!{err}'
return len(objs), ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.asc() if asc else order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return PlanDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def plan_stats(session, plan_id):
"""聚合计划执行进度,用于计划详情、进度看板和报告生成。"""
total = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id)).scalar() or 0
passed = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 1).scalar() or 0
failed = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 2).scalar() or 0
blocked = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 3).scalar() or 0
completed = passed + failed + blocked
pass_rate = round(passed / total * 100, 2) if total else 0
return {'total_cases': total, 'completed': completed, 'passed': passed, 'failed': failed, 'blocked': blocked, 'pass_rate': pass_rate}
@staticmethod
def plan_model():
return TestPlan
@staticmethod
def plan_case_model():
return PlanCase
@staticmethod
def round_model():
return TestRound

55
app/api/dao/productDao.py Normal file
View File

@@ -0,0 +1,55 @@
# encoding: UTF-8
from ..model.productModel import Product
from logger import logger
class ProductDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProductDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def product_model():
return Product

79
app/api/dao/projectDao.py Normal file
View File

@@ -0,0 +1,79 @@
# encoding: UTF-8
from ..model.productModel import Product
from ..model.projectModel import Environment, Project, ProjectMember
from logger import logger
class ProjectDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
"""按过滤条件分页查询;存在 is_delete 字段时统一过滤未删除数据。"""
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProjectDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def get_product_map(session, product_ids):
if not product_ids:
return {}
product_items = session.query(Product).filter(Product.id.in_(product_ids), Product.is_delete == 0).all()
return {product.id: product.name for product in product_items}
@staticmethod
def get_project_name_map(session, project_ids):
if not project_ids:
return {}
project_items = session.query(Project).filter(Project.id.in_(project_ids), Project.is_delete == 0).all()
return {project.id: {'name': project.name} for project in project_items}
@staticmethod
def project_model():
return Project
@staticmethod
def member_model():
return ProjectMember
@staticmethod
def environment_model():
return Environment

View File

@@ -0,0 +1,62 @@
# encoding: UTF-8
from ..model.projectHookModel import ProjectHook
from logger import logger
class ProjectHookDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProjectHookDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def hook_model():
return ProjectHook
@staticmethod
def list_all_by_filters(session, model_cls, filter_list):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
return query.all()

185
app/api/dao/rbacDao.py Normal file
View File

@@ -0,0 +1,185 @@
# encoding: UTF-8
from ..model.rbacModel import Role, Permission, RolePermission, Menu, RoleMenu
from logger import logger
class RbacDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return RbacDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def get_role_permission_ids(session, role_id):
items = session.query(RolePermission).filter(RolePermission.role_id == int(role_id), RolePermission.is_delete == 0).all()
return [item.permission_id for item in items]
@staticmethod
def replace_role_permissions(session, role_id, permission_ids):
role_id = int(role_id)
normalized_permission_ids = []
for permission_id in permission_ids:
permission_id = int(permission_id)
if permission_id not in normalized_permission_ids:
normalized_permission_ids.append(permission_id)
session.query(RolePermission).filter(RolePermission.role_id == role_id, RolePermission.is_delete == 0).update({'is_delete': 1})
session.flush()
if normalized_permission_ids:
existing_items = session.query(RolePermission).filter(
RolePermission.role_id == role_id,
RolePermission.permission_id.in_(normalized_permission_ids)
).all()
existing_map = {item.permission_id: item for item in existing_items}
for permission_id in normalized_permission_ids:
existing_item = existing_map.get(permission_id)
if existing_item:
existing_item.is_delete = 0
else:
session.add(RolePermission(role_id=role_id, permission_id=permission_id, is_delete=0))
err = session.done(close=False)
if err:
return 0, f'分配权限失败!{err}'
return role_id, ''
@staticmethod
def assign_permissions_to_roles(session, role_ids, permission_id):
permission_id = int(permission_id)
normalized_role_ids = []
for role_id in role_ids:
role_id = int(role_id)
if role_id not in normalized_role_ids:
normalized_role_ids.append(role_id)
existing_items = session.query(RolePermission).filter(
RolePermission.role_id.in_(normalized_role_ids),
RolePermission.permission_id == permission_id
).all()
existing_role_ids = {item.role_id for item in existing_items}
for role_id in normalized_role_ids:
if role_id not in existing_role_ids:
session.add(RolePermission(role_id=role_id, permission_id=permission_id, is_delete=0))
else:
existing_item = next(item for item in existing_items if item.role_id == role_id)
if existing_item.is_delete == 1:
existing_item.is_delete = 0
err = session.done(close=False)
if err:
return 0, f'分配权限失败!{err}'
return len(normalized_role_ids), ''
@staticmethod
def get_role_menu_ids(session, role_id):
items = session.query(RoleMenu).filter(RoleMenu.role_id == int(role_id), RoleMenu.is_delete == 0).all()
return [item.menu_id for item in items]
@staticmethod
def replace_role_menus(session, role_id, menu_ids):
role_id = int(role_id)
normalized_menu_ids = []
for menu_id in menu_ids:
menu_id = int(menu_id)
if menu_id not in normalized_menu_ids:
normalized_menu_ids.append(menu_id)
session.query(RoleMenu).filter(RoleMenu.role_id == role_id, RoleMenu.is_delete == 0).update({'is_delete': 1})
if normalized_menu_ids:
existing_items = session.query(RoleMenu).filter(
RoleMenu.role_id == role_id,
RoleMenu.menu_id.in_(normalized_menu_ids)
).all()
existing_map = {item.menu_id: item for item in existing_items}
for menu_id in normalized_menu_ids:
existing_item = existing_map.get(menu_id)
if existing_item:
existing_item.is_delete = 0
else:
session.add(RoleMenu(role_id=role_id, menu_id=menu_id, is_delete=0))
err = session.done(close=False)
if err:
return 0, f'分配菜单失败!{err}'
return role_id, ''
@staticmethod
def get_role_names_map(session, role_ids):
if not role_ids:
return {}
items = session.query(Role).filter(Role.id.in_(role_ids), Role.is_delete == 0).all()
return {item.id: item.name for item in items}
@staticmethod
def get_role_permission_codes(session, role_ids):
if not role_ids:
return []
permission_items = session.query(Permission.code).join(
RolePermission, RolePermission.permission_id == Permission.id
).filter(
RolePermission.role_id.in_(role_ids), RolePermission.is_delete == 0,
Permission.is_delete == 0, Permission.status == 1
).all()
menu_items = session.query(Menu.permission_code).join(
RoleMenu, RoleMenu.menu_id == Menu.id
).filter(
RoleMenu.role_id.in_(role_ids), RoleMenu.is_delete == 0,
Menu.is_delete == 0, Menu.status == 1
).all()
return sorted(list({item[0] for item in permission_items if item[0]} | {item[0] for item in menu_items if item[0]}))
@staticmethod
def get_menu_tree_items(session, filter_list):
return session.query(Menu).filter(*filter_list, Menu.is_delete == 0).order_by(Menu.sort.asc(), Menu.id.asc()).all()
@staticmethod
def role_model():
return Role
@staticmethod
def permission_model():
return Permission
@staticmethod
def menu_model():
return Menu
@staticmethod
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}

36
app/api/dao/reportDao.py Normal file
View File

@@ -0,0 +1,36 @@
# encoding: UTF-8
from ..model.reportModel import DefectSync, Report
from logger import logger
class ReportDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def get_by_id(session, model_cls, obj_id):
return session.query(model_cls).filter(model_cls.id == int(obj_id)).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
query = session.query(model_cls).filter(*filter_list)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.asc() if asc else order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def report_model():
return Report
@staticmethod
def defect_model():
return DefectSync

View File

@@ -43,15 +43,12 @@ class UpdateSqlProjectDao(object):
@staticmethod
def get_sql_by_filters(session, filter_list, page=1, limit=20):
rets = session.query(UpdateSqlProject)\
.filter(*filter_list) \
.filter(UpdateSqlProject.is_delete == 0) \
.order_by(UpdateSqlProject.created_time.desc()) \
query = session.query(UpdateSqlProject).filter(*filter_list).filter(UpdateSqlProject.is_delete == 0)
total = query.count()
rets = query.order_by(UpdateSqlProject.created_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(UpdateSqlProject).filter(*filter_list).filter(
UpdateSqlProject.is_delete == 0).count()
return rets, total
@staticmethod

109
app/api/dao/userDao.py Normal file
View File

@@ -0,0 +1,109 @@
# encoding: UTF-8
from datetime import datetime
from ..model.userModel import User, UserRole
from logger import logger
class UserDao(object):
@staticmethod
def create(session, model_cls, add_info):
obj = model_cls(**add_info)
session.add(obj)
err = session.done(close=False)
if err:
logger.warning(f'{model_cls.__name__}新增失败!{err}')
return 0, f'新增失败!{err}'
return obj.id, ''
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
update_res = session.query(model_cls).filter(*filters).update(update_info)
err = session.done(close=False)
if err:
logger.error(f'{model_cls.__name__}更新失败id: {obj_id}, err: {err}')
return 0, f'更新失败!{err}'
if not update_res:
return 0, '未查询到对应记录!'
return int(obj_id), ''
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
filters = [model_cls.id == int(obj_id)]
if soft_delete and hasattr(model_cls, 'is_delete'):
filters.append(model_cls.is_delete == 0)
return session.query(model_cls).filter(*filters).first()
@staticmethod
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
query = session.query(model_cls).filter(*filter_list)
if hasattr(model_cls, 'is_delete'):
query = query.filter(model_cls.is_delete == 0)
total = query.count()
if order_column is not None:
query = query.order_by(order_column.desc())
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
return rets, total
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return UserDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
@staticmethod
def get_user_role_ids(session, user_id):
items = session.query(UserRole).filter(UserRole.user_id == int(user_id), UserRole.is_delete == 0).all()
return [item.role_id for item in items]
@staticmethod
def replace_user_roles(session, user_id, role_ids):
user_id = int(user_id)
role_ids = [int(role_id) for role_id in role_ids]
session.query(UserRole).filter(UserRole.user_id == user_id, UserRole.is_delete == 0).update({'is_delete': 1})
existing_items = session.query(UserRole).filter(UserRole.user_id == user_id).all()
existing_map = {item.role_id: item for item in existing_items}
for role_id in role_ids:
existing_item = existing_map.get(role_id)
if existing_item:
existing_item.is_delete = 0
else:
session.add(UserRole(user_id=user_id, role_id=role_id, is_delete=0))
err = session.done(close=False)
if err:
return 0, f'分配角色失败!{err}'
return user_id, ''
@staticmethod
def get_user_roles(session, user_ids):
if not user_ids:
return {}
items = session.query(UserRole).filter(UserRole.user_id.in_(user_ids), UserRole.is_delete == 0).all()
ret = {}
for item in items:
ret.setdefault(item.user_id, []).append(item.role_id)
return ret
@staticmethod
def get_by_username(session, username):
return session.query(User).filter(User.username == username, User.is_delete == 0).first()
@staticmethod
def update_last_login_time(session, user_id):
session.query(User).filter(User.id == int(user_id), User.is_delete == 0).update({'last_login_time': datetime.now()})
err = session.done(close=False)
if err:
return 0, f'更新登录时间失败!{err}'
return int(user_id), ''
@staticmethod
def user_model():
return User
@staticmethod
def get_user_info_map(session, user_ids):
if not user_ids:
return {}
items = session.query(User).filter(User.id.in_(user_ids), User.is_delete == 0).all()
return {item.id: {'username': item.username, 'real_name': item.real_name} for item in items}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

58
app/api/model/bugModel.py Normal file
View File

@@ -0,0 +1,58 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, Text, TIMESTAMP, 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 Bug(Base):
__tablename__ = 'bug'
id = Column(BigInteger, primary_key=True, autoincrement=True)
bug_key = Column(String(64), nullable=False, unique=True)
title = Column(String(256), nullable=False)
description = Column(Text)
bug_type = Column(SmallInteger, nullable=False, default=1)
severity = Column(SmallInteger, nullable=False, default=2)
priority = Column(SmallInteger, nullable=False, default=2)
status = Column(SmallInteger, nullable=False, default=0)
assignee_id = Column(BigInteger)
reporter_id = Column(BigInteger, nullable=False)
product_id = Column(BigInteger, nullable=False)
project_id = Column(BigInteger, nullable=False)
module_id = Column(BigInteger)
case_id = Column(BigInteger)
plan_id = Column(BigInteger)
environment = Column(String(64))
steps = Column(Text)
solution = Column(Text)
resolve_version = Column(String(64))
resolved_by = Column(BigInteger)
reproduce_rate = Column(SmallInteger)
attachments = Column(JSONB, server_default=text("'[]'::jsonb"))
is_delete = Column(Integer, default=0)
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True)
class BugComment(Base):
__tablename__ = 'bug_comment'
id = Column(BigInteger, primary_key=True, autoincrement=True)
bug_id = Column(BigInteger, nullable=False)
content = Column(Text, nullable=False)
user_id = Column(BigInteger, nullable=False)
is_delete = Column(Integer, default=0)
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)
class BugHistory(Base):
__tablename__ = 'bug_history'
id = Column(BigInteger, primary_key=True, autoincrement=True)
bug_id = Column(BigInteger, nullable=False)
field_name = Column(String(64), nullable=False)
old_value = Column(String(512))
new_value = Column(String(512))
operator_id = Column(BigInteger, nullable=False)
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)

View File

@@ -0,0 +1,62 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.ext.declarative import declarative_base
from common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class Module(Base):
__tablename__ = 'module'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
parent_id = Column(BigInteger, default=0, comment='父模块id')
name = Column(String(128), nullable=False, comment='模块名称')
sort_order = Column(Integer, default=0, comment='排序')
path = Column(String(512), comment='模块路径')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
class TestCase(Base):
__tablename__ = 'test_case'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
module_id = Column(BigInteger, comment='模块id')
case_key = Column(String(64), nullable=False, comment='项目内唯一编号')
title = Column(String(255), nullable=False, comment='标题')
preconditions = Column(Text, comment='前置条件')
steps = Column(Text, comment='步骤')
expected_results = Column(Text, comment='预期结果')
priority = Column(SmallInteger, default=2, comment='0:P0 1:P1 2:P2 3:P3')
case_type = Column(SmallInteger, default=1, comment='1:功能 2:性能 3:安全 4:接口')
tags = Column(ARRAY(String(64)), server_default=text("'{}'::varchar[]"), comment='标签')
status = Column(SmallInteger, default=1, comment='1:正常 2:已废弃 3:评审中 4评审通过')
is_auto = Column(Integer, default=0, comment='0未实现自动化1已实现自动化')
created_by = Column(BigInteger, comment='创建人')
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='修改时间')
class CaseSnapshot(Base):
__tablename__ = 'case_snapshot'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
case_id = Column(BigInteger, nullable=False, comment='用例id')
version = Column(Integer, nullable=False, comment='版本')
snapshot = Column(JSONB, nullable=False, comment='快照')
created_by = Column(BigInteger, comment='创建人')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
class CaseReview(Base):
__tablename__ = 'case_review'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
case_id = Column(BigInteger, nullable=False, comment='用例id')
reviewer_id = Column(BigInteger, nullable=False, comment='评审人')
status = Column(SmallInteger, default=0, comment='0:待评审 1:通过 2:驳回 3:建议修改')
comments = Column(Text, comment='评论')
diff_content = Column(Text, comment='JSON diff')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
reviewed_time = Column(TIMESTAMP, nullable=True, comment='评审时间')

View File

@@ -0,0 +1,38 @@
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 DataBuilder(Base):
__tablename__ = 'data_builder'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
name = Column(String(128), nullable=False, comment='造数器名称')
description = Column(Text, comment='描述')
builder_type = Column(SmallInteger, default=1, comment='1:流程编排 2:SQL 3:脚本')
definition = Column(JSONB, nullable=False, comment='构造定义')
input_schema = Column(JSONB, comment='输入定义')
output_example = Column(JSONB, comment='输出示例')
created_by = Column(BigInteger, comment='创建人')
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='修改时间')
class DataTask(Base):
__tablename__ = 'data_task'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
builder_id = Column(BigInteger, nullable=False, comment='造数器id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
params = Column(JSONB, comment='任务参数')
status = Column(SmallInteger, default=0, comment='0:等待 1:执行中 2:成功 3:失败')
result_data = Column(JSONB, comment='生成数据')
error_message = Column(Text, comment='错误信息')
created_by = Column(BigInteger, comment='创建人')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
completed_time = Column(TIMESTAMP, nullable=True, comment='完成时间')

View File

@@ -0,0 +1,50 @@
from sqlalchemy import BigInteger, Column, Date, 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 TestPlan(Base):
__tablename__ = 'test_plan'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
name = Column(String(128), nullable=False, comment='计划名称')
version = Column(String(32), comment='测试版本')
description = Column(Text, comment='描述')
start_date = Column(Date, comment='开始日期')
end_date = Column(Date, comment='结束日期')
owner_id = Column(BigInteger, comment='负责人')
status = Column(SmallInteger, default=0, comment='0:草稿 1:进行中 2:已完成 3:已归档 4已通过')
environment_id = Column(BigInteger, comment='环境id')
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='修改时间')
class PlanCase(Base):
__tablename__ = 'plan_case'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
plan_id = Column(BigInteger, nullable=False, comment='计划id')
case_id = Column(BigInteger, nullable=False, comment='用例id')
assignee_id = Column(BigInteger, comment='执行人')
round_no = Column(Integer, default=1, comment='执行轮次')
status = Column(SmallInteger, default=0, comment='0:未开始 1:通过 2:失败 3:阻塞')
actual_result = Column(Text, comment='实际结果')
defect_links = Column(JSONB, server_default=text("'[]'::jsonb"), comment='缺陷链接')
attachments = Column(JSONB, server_default=text("'[]'::jsonb"), comment='附件')
executed_time = Column(TIMESTAMP, comment='执行时间')
execution_duration = Column(Integer, comment='执行耗时')
role_name_map = {1: '测试经理', 2: '测试工程师', 3: '开发工程师', 4: '访客'}
class TestRound(Base):
__tablename__ = 'test_round'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
plan_id = Column(BigInteger, nullable=False, comment='计划id')
round_no = Column(Integer, nullable=False, comment='轮次')
name = Column(String(64), comment='轮次名称')
start_date = Column(Date, comment='开始日期')
end_date = Column(Date, comment='结束日期')

View File

@@ -0,0 +1,19 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
from sqlalchemy.ext.declarative import declarative_base
from common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class Product(Base):
__tablename__ = 'product'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
name = Column(String(128), nullable=False, comment='产品名称')
code = Column(String(64), unique=True, nullable=False, comment='产品编码')
description = Column(Text, comment='产品描述')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
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,23 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, Text, TIMESTAMP, 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 ProjectHook(Base):
__tablename__ = 'project_hook'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
hook_type = Column(SmallInteger, nullable=False, comment='1:飞书 2:钉钉 3:企微')
webhook_url = Column(String(512), nullable=False, comment='webhook地址')
secret = Column(String(256), comment='签名密钥')
enabled = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
description = Column(String(256), comment='描述')
config = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展配置')
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,44 @@
from sqlalchemy import BigInteger, Boolean, 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 Project(Base):
__tablename__ = 'project'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
key = Column(String(32), unique=True, nullable=False, comment='项目唯一标识')
name = Column(String(128), nullable=False, comment='项目名称')
product_id = Column(Integer, comment='产品id')
description = Column(Text, comment='项目描述')
department = Column(String(64), comment='部门')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
config = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展配置')
created_by = Column(BigInteger, comment='创建人')
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='修改时间')
class ProjectMember(Base):
__tablename__ = 'project_member'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
user_id = Column(BigInteger, nullable=False, comment='用户id')
role = Column(SmallInteger, nullable=False, comment='1:测试经理 2:测试工程师 3:开发工程师 4:访客')
joined_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='加入时间')
class Environment(Base):
__tablename__ = 'environment'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
name = Column(String(64), nullable=False, comment='环境名称,如 dev/st/pre/prod')
variables = Column(JSONB, nullable=False, comment='环境变量')
is_encrypted = Column(Boolean, default=False, comment='是否加密')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')

View File

@@ -0,0 +1,72 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
from sqlalchemy.ext.declarative import declarative_base
from common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class Role(Base):
__tablename__ = 'role'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
code = Column(String(64), unique=True, nullable=False, comment='角色编码')
name = Column(String(64), nullable=False, comment='角色名称')
description = Column(Text, comment='角色描述')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
is_system = Column(SmallInteger, default=0, comment='是否系统内置角色')
created_by = Column(BigInteger, comment='创建人')
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='修改时间')
class Permission(Base):
__tablename__ = 'permission'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
code = Column(String(128), unique=True, nullable=False, comment='权限编码')
name = Column(String(128), nullable=False, comment='权限名称')
module = Column(String(64), comment='所属模块')
action = Column(String(64), comment='动作')
description = Column(Text, comment='描述')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
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='修改时间')
class RolePermission(Base):
__tablename__ = 'role_permission'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
role_id = Column(BigInteger, nullable=False, comment='角色id')
permission_id = Column(BigInteger, nullable=False, comment='权限id')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
class Menu(Base):
__tablename__ = 'menu'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
parent_id = Column(BigInteger, default=0, comment='父菜单id')
name = Column(String(64), nullable=False, comment='菜单名称')
code = Column(String(64), unique=True, comment='菜单编码')
type = Column(SmallInteger, default=1, comment='1:目录 2:菜单 3:按钮')
path = Column(String(255), comment='路由路径')
component = Column(String(255), comment='前端组件路径')
icon = Column(String(64), comment='图标')
permission_code = Column(String(128), comment='对应权限编码')
sort = Column(Integer, default=0, comment='排序')
visible = Column(SmallInteger, default=1, comment='1:显示 0:隐藏')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
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='修改时间')
class RoleMenu(Base):
__tablename__ = 'role_menu'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
role_id = Column(BigInteger, nullable=False, comment='角色id')
menu_id = Column(BigInteger, nullable=False, comment='菜单id')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')

View File

@@ -0,0 +1,34 @@
from sqlalchemy import BigInteger, Column, 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 Report(Base):
__tablename__ = 'report'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
plan_id = Column(BigInteger, nullable=False, comment='计划id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
product_id = Column(BigInteger, nullable=False, comment='产品id')
name = Column(String(128), nullable=False, comment='报告名称')
report_type = Column(SmallInteger, default=1, comment='1:实时报告 2:归档报告')
summary = Column(JSONB, comment='统计数据')
content = Column(Text, comment='HTML内容')
file_url = Column(String(512), comment='文件地址')
generated_by = Column(BigInteger, comment='生成人')
generated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='生成时间')
class DefectSync(Base):
__tablename__ = 'defect_sync'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
project_id = Column(BigInteger, nullable=False, comment='项目id')
external_id = Column(String(64), nullable=False, comment='外部缺陷id')
external_system = Column(String(32), comment='外部系统')
plan_case_id = Column(BigInteger, comment='计划用例id')
status = Column(String(32), comment='外部状态')
last_sync_time = Column(TIMESTAMP, comment='最后同步时间')

View File

@@ -25,6 +25,3 @@ class UpdateSqlProject(Base):
nullable=True,
comment='修改时间'
)
def __repr__(self):
return '<update_sql_project %r>' % self.id

View File

@@ -0,0 +1,33 @@
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, text
from sqlalchemy.ext.declarative import declarative_base
from common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class User(Base):
__tablename__ = 'user'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
username = Column(String(64), unique=True, nullable=False, comment='登录用户名')
real_name = Column(String(64), comment='真实姓名')
password_hash = Column(String(255), nullable=False, comment='密码哈希')
mobile = Column(String(32), comment='手机号')
email = Column(String(128), comment='邮箱')
avatar = Column(String(255), comment='头像地址')
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
last_login_time = Column(TIMESTAMP, nullable=True, comment='最后登录时间')
created_by = Column(BigInteger, comment='创建人')
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='修改时间')
class UserRole(Base):
__tablename__ = 'user_role'
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
user_id = Column(BigInteger, nullable=False, comment='用户id')
role_id = Column(BigInteger, nullable=False, comment='角色id')
is_delete = Column(Integer, default=0, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,55 @@
# encoding: UTF-8
from ..dao.bugDao import BugDao
from ..model.bugModel import BugComment
class BugService(object):
"""Bug 管理 Service 层"""
@staticmethod
def create(session, model_cls, add_info):
return BugDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return BugDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return BugDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False):
return BugDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return BugDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def get_comments(session, bug_id):
return BugDao.get_comments(session, bug_id)
@staticmethod
def get_history(session, bug_id):
return BugDao.get_history(session, bug_id)
@staticmethod
def add_comment(session, bug_id, content, user_id):
return BugDao.create(session, BugComment, {
'bug_id': bug_id,
'content': content,
'user_id': user_id
})
@staticmethod
def generate_bug_key(session):
return BugDao.generate_bug_key(session)
@staticmethod
def get_stats(session, product_id=None, project_id=None):
return BugDao.get_stats(session, product_id, project_id)
@staticmethod
def add_history(session, bug_id, field_name, old_value, new_value, operator_id):
return BugDao.add_history(session, bug_id, field_name, old_value, new_value, operator_id)

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
from ..dao.caseDao import CaseDao
class CaseService(object):
"""用例域 Service 层,封装用例编号和快照版本等业务能力。"""
@staticmethod
def create(session, model_cls, add_info):
return CaseDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return CaseDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return CaseDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return CaseDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
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)
@staticmethod
def next_snapshot_version(session, case_id):
return CaseDao.next_snapshot_version(session, case_id)
@staticmethod
def get_module_name_map(session, module_ids):
return CaseDao.get_module_name_map(session, module_ids)

View File

@@ -0,0 +1,63 @@
# encoding: UTF-8
from datetime import datetime
from common.dataBuilderExecutor import DataBuilderExecutor
from ..dao.dataBuilderDao import DataBuilderDao
from ..model.dataBuilderModel import DataBuilder, DataTask
class DataBuilderService(object):
@staticmethod
def create(session, model_cls, add_info):
return DataBuilderDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return DataBuilderDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return DataBuilderDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return DataBuilderDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return DataBuilderDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def execute_builder(session, builder_id, params=None, created_by=None):
builder = DataBuilderDao.get_by_id(session, DataBuilder, builder_id)
if not builder:
return {}, '未查询到对应造数器!'
params = params or {}
task_info = {
'builder_id': builder.id,
'project_id': builder.project_id,
'params': params,
'status': 1,
'created_by': created_by
}
# 先写入执行中任务,保证失败时也能追踪任务记录。
task_id, err_msg = DataBuilderDao.create(session, DataTask, task_info)
if err_msg:
return {}, err_msg
try:
# 当前 MVP 只做同步模板渲染执行,后续可在 executor 内扩展 http/db step。
executor = DataBuilderExecutor(builder.definition or {}, {})
result_data = executor.execute(params)
DataBuilderDao.update_by_id(session, DataTask, task_id, {
'status': 2,
'result_data': result_data,
'completed_time': datetime.now()
}, soft_delete=False)
return {'taskId': task_id, 'data': result_data}, ''
except Exception as e:
DataBuilderDao.update_by_id(session, DataTask, task_id, {
'status': 3,
'error_message': str(e),
'completed_time': datetime.now()
}, soft_delete=False)
return {}, f'执行造数失败!{e}'

View File

@@ -0,0 +1,34 @@
# encoding: UTF-8
from ..dao.planDao import PlanDao
class PlanService(object):
"""测试计划域 Service 层,封装计划统计等业务能力。"""
@staticmethod
def create(session, model_cls, add_info):
return PlanDao.create(session, model_cls, add_info)
@staticmethod
def batch_create(session, model_cls, batch_info_list):
return PlanDao.batch_create(session, model_cls, batch_info_list)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return PlanDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return PlanDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False):
return PlanDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return PlanDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def plan_stats(session, plan_id):
return PlanDao.plan_stats(session, plan_id)

View File

@@ -0,0 +1,24 @@
# encoding: UTF-8
from ..dao.productDao import ProductDao
class ProductService(object):
@staticmethod
def create(session, model_cls, add_info):
return ProductDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return ProductDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return ProductDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return ProductDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProductDao.delete_by_id(session, model_cls, obj_id)

View File

@@ -0,0 +1,36 @@
# encoding: UTF-8
from ..dao.projectHookDao import ProjectHookDao
from ..model.projectHookModel import ProjectHook
class ProjectHookService(object):
@staticmethod
def create(session, model_cls, add_info):
return ProjectHookDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return ProjectHookDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return ProjectHookDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return ProjectHookDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProjectHookDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def get_hooks_by_project(session, project_id, hook_type=None):
filters = [
ProjectHook.project_id == int(project_id),
ProjectHook.is_delete == 0,
ProjectHook.enabled == 1
]
if hook_type not in (None, ''):
filters.append(ProjectHook.hook_type == int(hook_type))
return ProjectHookDao.list_all_by_filters(session, ProjectHook, filters)

View File

@@ -0,0 +1,34 @@
# encoding: UTF-8
from ..dao.projectDao import ProjectDao
class ProjectService(object):
"""项目域 Service 层,保持业务入口与 DAO 解耦。"""
@staticmethod
def create(session, model_cls, add_info):
return ProjectDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return ProjectDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return ProjectDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return ProjectDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return ProjectDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def get_product_map(session, product_ids):
return ProjectDao.get_product_map(session, product_ids)
@staticmethod
def get_project_name_map(session, project_ids):
return ProjectDao.get_project_name_map(session, project_ids)

View File

@@ -0,0 +1,103 @@
# encoding: UTF-8
from ..dao.rbacDao import RbacDao
def has_permission(permission_code, permission_codes):
if not permission_code:
return True
if not permission_codes:
return False
if permission_code in permission_codes:
return True
if '*:*' in permission_codes:
return True
if ':' in permission_code:
module_code = permission_code.split(':', 1)[0]
if f'{module_code}:*' in permission_codes:
return True
if '_' in module_code:
parent_module_code = module_code.split('_', 1)[0]
if f'{parent_module_code}:*' in permission_codes:
return True
return False
class RbacService(object):
@staticmethod
def create(session, model_cls, add_info):
return RbacDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return RbacDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return RbacDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return RbacDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return RbacDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def assign_permissions(session, role_ids, permission_id):
return RbacDao.assign_permissions_to_roles(session, role_ids, permission_id)
@staticmethod
def assign_menus(session, role_id, menu_ids):
return RbacDao.replace_role_menus(session, role_id, menu_ids)
@staticmethod
def get_role_permission_ids(session, role_id):
return RbacDao.get_role_permission_ids(session, role_id)
@staticmethod
def get_role_menu_ids(session, role_id):
return RbacDao.get_role_menu_ids(session, role_id)
@staticmethod
def build_menu_tree(session, filters, role_ids=None, menu_ids=None):
items = RbacDao.get_menu_tree_items(session, filters)
visible_ids = set()
if not role_ids and not menu_ids:
visible_ids = {item.id for item in items}
else:
role_menu_ids = set(menu_ids or [])
if role_ids:
for role_id in role_ids:
role_menu_ids.update(RbacDao.get_role_menu_ids(session, role_id))
visible_ids = set(role_menu_ids)
item_by_id = {item.id: item for item in items}
for item_id in list(visible_ids):
if item_id not in item_by_id:
continue
parent_id = item_by_id[item_id].parent_id
while parent_id and parent_id in item_by_id:
if parent_id in visible_ids:
break
visible_ids.add(parent_id)
parent_id = item_by_id[parent_id].parent_id
item_map = {}
roots = []
for item in items:
if item.id not in visible_ids:
continue
item_dict = item.to_dict()
item_dict['children'] = []
item_map[item.id] = item_dict
for item in items:
if item.id not in item_map:
continue
if item.parent_id and item.parent_id in item_map:
item_map[item.parent_id]['children'].append(item_map[item.id])
else:
roots.append(item_map[item.id])
return roots
@staticmethod
def get_role_permission_codes(session, role_ids):
return RbacDao.get_role_permission_codes(session, role_ids)

View File

@@ -0,0 +1,46 @@
# encoding: UTF-8
from ..dao.planDao import PlanDao
from ..dao.projectDao import ProjectDao
from ..dao.reportDao import ReportDao
from ..model.planModel import TestPlan
from ..model.reportModel import Report
class ReportService(object):
@staticmethod
def create(session, model_cls, add_info):
return ReportDao.create(session, model_cls, add_info)
@staticmethod
def get_by_id(session, model_cls, obj_id):
return ReportDao.get_by_id(session, model_cls, obj_id)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False):
return ReportDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc)
@staticmethod
def generate_report(session, plan_id, generated_by=None):
plan = PlanDao.get_by_id(session, TestPlan, plan_id)
if not plan:
return 0, '未查询到对应计划!'
project = ProjectDao.get_by_id(session, ProjectDao.project_model(), plan.project_id)
if not project:
return 0, '未查询到对应项目!'
# 复用计划统计,保证计划详情和报告中的指标口径一致。
stats = PlanDao.plan_stats(session, plan_id)
# MVP 阶段先生成简单 HTML后续可替换为模板渲染器。
content = '<html><body><h1>{}</h1><p>总用例:{}</p><p>通过率:{}%</p></body></html>'.format(
plan.name, stats['total_cases'], stats['pass_rate']
)
add_info = {
'plan_id': int(plan_id),
'project_id': plan.project_id,
'product_id': project.product_id,
'name': '{}_报告'.format(plan.name),
'report_type': 1,
'summary': stats,
'content': content,
'generated_by': generated_by
}
return ReportDao.create(session, Report, add_info)

View File

@@ -0,0 +1,58 @@
# encoding: UTF-8
from ..dao.userDao import UserDao
from ..dao.rbacDao import RbacDao
class UserService(object):
@staticmethod
def create(session, model_cls, add_info):
return UserDao.create(session, model_cls, add_info)
@staticmethod
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
return UserDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
@staticmethod
def get_by_id(session, model_cls, obj_id, soft_delete=True):
return UserDao.get_by_id(session, model_cls, obj_id, soft_delete)
@staticmethod
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
return UserDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
@staticmethod
def delete_by_id(session, model_cls, obj_id):
return UserDao.delete_by_id(session, model_cls, obj_id)
@staticmethod
def assign_roles(session, user_id, role_ids):
return UserDao.replace_user_roles(session, user_id, role_ids)
@staticmethod
def get_user_role_ids(session, user_id):
return UserDao.get_user_role_ids(session, user_id)
@staticmethod
def get_user_roles_map(session, user_ids):
user_role_map = UserDao.get_user_roles(session, user_ids)
role_ids = list({role_id for role_list in user_role_map.values() for role_id in role_list})
role_name_map = RbacDao.get_role_names_map(session, role_ids)
ret = {}
for user_id, ids in user_role_map.items():
ret[user_id] = {
'role_ids': ids,
'role_names': [role_name_map.get(role_id, '') for role_id in ids]
}
return ret
@staticmethod
def get_by_username(session, username):
return UserDao.get_by_username(session, username)
@staticmethod
def get_user_info_map(session, user_ids):
return UserDao.get_user_info_map(session, user_ids)
@staticmethod
def update_last_login_time(session, user_id):
return UserDao.update_last_login_time(session, user_id)

Binary file not shown.

View File

@@ -0,0 +1,152 @@
# encoding: UTF-8
import json
import uuid
from functools import wraps
import redis
from sqlalchemy.exc import OperationalError
from flask import request, g
from const import REDIS_URL
from common.apiResponse import ApiResponse
from ..service.userService import UserService
from ..service.rbacService import RbacService
from ..model.userModel import User
from common.sqlSession import SqlSession
TOKEN_PREFIX = 'effekt:token:'
TOKEN_CONTEXT_PREFIX = 'effekt:token:ctx:'
TOKEN_EXPIRE_SECONDS = 7200
TOKEN_REFRESH_THRESHOLD_SECONDS = 1800
TOKEN_CONTEXT_EXPIRE_SECONDS = 300
WHITELIST_PATHS = ['/it/api/auth/login', '/it/api/auth/register']
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
_redis_client.ping()
def create_token(user_id):
token = uuid.uuid4().hex
key = TOKEN_PREFIX + token
_redis_client.setex(key, TOKEN_EXPIRE_SECONDS, str(user_id))
return token, TOKEN_EXPIRE_SECONDS
def get_token_ttl(token):
return _redis_client.ttl(TOKEN_PREFIX + token)
def refresh_token_if_needed(token):
ttl = get_token_ttl(token)
if ttl != -2 and ttl < TOKEN_REFRESH_THRESHOLD_SECONDS:
_redis_client.expire(TOKEN_PREFIX + token, TOKEN_EXPIRE_SECONDS)
return TOKEN_EXPIRE_SECONDS
return ttl
def get_current_user_id(token):
user_id = _redis_client.get(TOKEN_PREFIX + token)
return int(user_id) if user_id else 0
def parse_token():
return request.headers.get('accessToken') or request.headers.get('accesstoken') or request.headers.get('Authorization', '').replace('Bearer ', '')
def get_token_context(token):
context_str = _redis_client.get(TOKEN_CONTEXT_PREFIX + token)
return json.loads(context_str) if context_str else None
def cache_token_context(token, user, role_ids, permission_codes):
_redis_client.setex(TOKEN_CONTEXT_PREFIX + token, TOKEN_CONTEXT_EXPIRE_SECONDS, json.dumps({
'user': user.to_dict(),
'role_ids': role_ids,
'permission_codes': permission_codes
}, default=str))
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
token = parse_token()
if not token:
return ApiResponse.build_failure(40004, msg='未登录或缺少token')
user_id = get_current_user_id(token)
if not user_id:
return ApiResponse.build_failure(40004, msg='token无效或已过期')
session = None
try:
token_context = get_token_context(token)
if token_context:
g.current_user_id = user_id
g.current_user = token_context.get('user', {})
g.current_role_ids = token_context.get('role_ids', [])
g.current_permission_codes = token_context.get('permission_codes', [])
g.current_token = token
g.current_token_ttl = refresh_token_if_needed(token)
return func(*args, **kwargs)
session = SqlSession()
user = UserService.get_by_id(session, User, user_id)
if not user:
return ApiResponse.build_failure(40011, msg='未查询到对应用户!')
role_ids = UserService.get_user_role_ids(session, user_id)
permission_codes = RbacService.get_role_permission_codes(session, role_ids)
cache_token_context(token, user, role_ids, permission_codes)
g.current_user_id = user_id
g.current_user = user
g.current_role_ids = role_ids
g.current_permission_codes = permission_codes
g.current_token = token
g.current_token_ttl = refresh_token_if_needed(token)
return func(*args, **kwargs)
except OperationalError:
return ApiResponse.build_failure(40008, msg='数据库连接超时,请稍后重试!')
finally:
if session:
session.close()
return wrapper
def has_permission(permission_code, permission_codes):
if not permission_code:
return True
if not permission_codes:
return False
if permission_code in permission_codes:
return True
if '*:*' in permission_codes:
return True
if ':' in permission_code:
module_code = permission_code.split(':', 1)[0]
if f'{module_code}:*' in permission_codes:
return True
if '_' in module_code:
parent_module_code = module_code.split('_', 1)[0]
if f'{parent_module_code}:*' in permission_codes:
return True
return False
def permission_required(permission_code):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not getattr(g, 'current_user_id', None):
return ApiResponse.build_failure(40004, 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 func(*args, **kwargs)
return wrapper
return decorator
def should_skip_auth(path):
return path in WHITELIST_PATHS
def logout_token(token):
if token:
_redis_client.delete(TOKEN_PREFIX + token)
_redis_client.delete(TOKEN_CONTEXT_PREFIX + token)

File diff suppressed because it is too large Load Diff