增加项目的各个功能

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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