增加项目的各个功能
This commit is contained in:
BIN
app/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/views.cpython-38.pyc
Normal file
BIN
app/api/__pycache__/views.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/baseCrudController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/baseCrudController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/bugController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/bugController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/caseController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/caseController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/planController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/planController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/productController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/productController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/projectController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/projectController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/rbacController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/rbacController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/reportController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/reportController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/userController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/userController.cpython-38.pyc
Normal file
Binary file not shown.
57
app/api/controller/baseCrudController.py
Normal file
57
app/api/controller/baseCrudController.py
Normal 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]
|
||||
325
app/api/controller/bugController.py
Normal file
325
app/api/controller/bugController.py
Normal 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)
|
||||
413
app/api/controller/caseController.py
Normal file
413
app/api/controller/caseController.py
Normal 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')
|
||||
80
app/api/controller/dataBuilderController.py
Normal file
80
app/api/controller/dataBuilderController.py
Normal 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), ''
|
||||
192
app/api/controller/planController.py
Normal file
192
app/api/controller/planController.py
Normal 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), ''
|
||||
64
app/api/controller/productController.py
Normal file
64
app/api/controller/productController.py
Normal 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)
|
||||
198
app/api/controller/projectController.py
Normal file
198
app/api/controller/projectController.py
Normal 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, ''
|
||||
225
app/api/controller/projectHookController.py
Normal file
225
app/api/controller/projectHookController.py
Normal 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)
|
||||
257
app/api/controller/rbacController.py
Normal file
257
app/api/controller/rbacController.py
Normal 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)
|
||||
47
app/api/controller/reportController.py
Normal file
47
app/api/controller/reportController.py
Normal 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'))
|
||||
128
app/api/controller/userController.py
Normal file
128
app/api/controller/userController.py
Normal 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, ''
|
||||
BIN
app/api/dao/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/bugDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/bugDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/caseDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/caseDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/dataBuilderDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/dataBuilderDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/planDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/planDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/productDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/productDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/projectDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/projectDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/projectHookDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/projectHookDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/rbacDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/rbacDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/reportDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/reportDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/updateSqlProjectDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/updateSqlProjectDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/userDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/userDao.cpython-38.pyc
Normal file
Binary file not shown.
224
app/api/dao/bugDao.py
Normal file
224
app/api/dao/bugDao.py
Normal 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
90
app/api/dao/caseDao.py
Normal 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}
|
||||
59
app/api/dao/dataBuilderDao.py
Normal file
59
app/api/dao/dataBuilderDao.py
Normal 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
88
app/api/dao/planDao.py
Normal 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
55
app/api/dao/productDao.py
Normal 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
79
app/api/dao/projectDao.py
Normal 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
|
||||
62
app/api/dao/projectHookDao.py
Normal file
62
app/api/dao/projectHookDao.py
Normal 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
185
app/api/dao/rbacDao.py
Normal 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
36
app/api/dao/reportDao.py
Normal 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
|
||||
@@ -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
109
app/api/dao/userDao.py
Normal 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}
|
||||
BIN
app/api/model/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/bugModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/bugModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/caseModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/caseModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/planModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/planModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/productModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/productModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/projectHookModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/projectHookModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/projectModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/projectModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/rbacModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/rbacModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/reportModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/reportModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/userModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/userModel.cpython-38.pyc
Normal file
Binary file not shown.
58
app/api/model/bugModel.py
Normal file
58
app/api/model/bugModel.py
Normal 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)
|
||||
62
app/api/model/caseModel.py
Normal file
62
app/api/model/caseModel.py
Normal 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='评审时间')
|
||||
38
app/api/model/dataBuilderModel.py
Normal file
38
app/api/model/dataBuilderModel.py
Normal 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='完成时间')
|
||||
50
app/api/model/planModel.py
Normal file
50
app/api/model/planModel.py
Normal 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='结束日期')
|
||||
19
app/api/model/productModel.py
Normal file
19
app/api/model/productModel.py
Normal 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='修改时间')
|
||||
23
app/api/model/projectHookModel.py
Normal file
23
app/api/model/projectHookModel.py
Normal 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='修改时间')
|
||||
44
app/api/model/projectModel.py
Normal file
44
app/api/model/projectModel.py
Normal 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='创建时间')
|
||||
72
app/api/model/rbacModel.py
Normal file
72
app/api/model/rbacModel.py
Normal 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='创建时间')
|
||||
34
app/api/model/reportModel.py
Normal file
34
app/api/model/reportModel.py
Normal 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='最后同步时间')
|
||||
@@ -25,6 +25,3 @@ class UpdateSqlProject(Base):
|
||||
nullable=True,
|
||||
comment='修改时间'
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return '<update_sql_project %r>' % self.id
|
||||
|
||||
33
app/api/model/userModel.py
Normal file
33
app/api/model/userModel.py
Normal 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='创建时间')
|
||||
BIN
app/api/service/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/bugService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/bugService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/caseService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/caseService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/dataBuilderService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/dataBuilderService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/planService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/planService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/productService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/productService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/projectHookService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/projectHookService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/projectService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/projectService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/rbacService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/rbacService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/reportService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/reportService.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/service/__pycache__/userService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/userService.cpython-38.pyc
Normal file
Binary file not shown.
55
app/api/service/bugService.py
Normal file
55
app/api/service/bugService.py
Normal 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)
|
||||
38
app/api/service/caseService.py
Normal file
38
app/api/service/caseService.py
Normal 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)
|
||||
63
app/api/service/dataBuilderService.py
Normal file
63
app/api/service/dataBuilderService.py
Normal 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}'
|
||||
34
app/api/service/planService.py
Normal file
34
app/api/service/planService.py
Normal 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)
|
||||
24
app/api/service/productService.py
Normal file
24
app/api/service/productService.py
Normal 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)
|
||||
36
app/api/service/projectHookService.py
Normal file
36
app/api/service/projectHookService.py
Normal 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)
|
||||
34
app/api/service/projectService.py
Normal file
34
app/api/service/projectService.py
Normal 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)
|
||||
103
app/api/service/rbacService.py
Normal file
103
app/api/service/rbacService.py
Normal 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)
|
||||
46
app/api/service/reportService.py
Normal file
46
app/api/service/reportService.py
Normal 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)
|
||||
58
app/api/service/userService.py
Normal file
58
app/api/service/userService.py
Normal 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)
|
||||
BIN
app/api/utils/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/utils/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/utils/__pycache__/authMiddleware.cpython-38.pyc
Normal file
BIN
app/api/utils/__pycache__/authMiddleware.cpython-38.pyc
Normal file
Binary file not shown.
152
app/api/utils/authMiddleware.py
Normal file
152
app/api/utils/authMiddleware.py
Normal 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)
|
||||
1312
app/api/views.py
1312
app/api/views.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user