mirror of
https://github.com/opsre/LiteOps.git
synced 2026-06-09 20:57:25 +08:00
first commit
This commit is contained in:
924
backend/apps/views/build.py
Normal file
924
backend/apps/views/build.py
Normal file
@@ -0,0 +1,924 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, F
|
||||
from ..models import BuildTask, BuildHistory, Project, Environment, GitlabTokenCredential, User, NotificationRobot
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.builder import Builder
|
||||
from ..utils.permissions import get_user_permissions
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
def execute_build(task, build_number, commit_id, history):
|
||||
"""执行构建任务"""
|
||||
try:
|
||||
builder = Builder(task, build_number, commit_id, history)
|
||||
builder.execute()
|
||||
finally:
|
||||
# 无论构建成功、失败或异常,都将构建状态重置为空闲
|
||||
from django.db import transaction
|
||||
with transaction.atomic():
|
||||
# 重新获取任务对象,确保获取最新状态
|
||||
from ..models import BuildTask
|
||||
BuildTask.objects.filter(task_id=task.task_id).update(building_status='idle')
|
||||
logger.info(f"任务 [{task.task_id}] 构建状态已重置为空闲")
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildTaskView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request, task_id=None):
|
||||
"""获取构建任务列表或单个任务详情"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有构建任务查看权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_permissions = function_permissions.get('build_task', [])
|
||||
|
||||
if 'view' not in build_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有构建任务查看权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 如果请求参数中包含get_robots=true,则返回通知机器人列表
|
||||
if request.GET.get('get_robots') == 'true':
|
||||
robots = NotificationRobot.objects.all()
|
||||
robot_list = []
|
||||
for robot in robots:
|
||||
robot_list.append({
|
||||
'robot_id': robot.robot_id,
|
||||
'type': robot.type,
|
||||
'name': robot.name,
|
||||
})
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取通知机器人列表成功',
|
||||
'data': robot_list
|
||||
})
|
||||
|
||||
# 如果提供了task_id,则返回单个任务详情
|
||||
if task_id:
|
||||
try:
|
||||
task = BuildTask.objects.select_related(
|
||||
'project',
|
||||
'environment',
|
||||
'git_token',
|
||||
'creator'
|
||||
).get(task_id=task_id)
|
||||
|
||||
# 检查是否有权限查看该任务(项目权限和环境权限)
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if task.project and task.project.project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{task.project.project_id}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该项目的构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if task.environment and task.environment.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{task.environment.type}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境的构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 获取最新的构建历史
|
||||
latest_build = BuildHistory.objects.filter(task=task).order_by('-build_number').first()
|
||||
|
||||
# 获取通知机器人详情
|
||||
notification_robots = []
|
||||
if task.notification_channels:
|
||||
robots = NotificationRobot.objects.filter(robot_id__in=task.notification_channels)
|
||||
for robot in robots:
|
||||
notification_robots.append({
|
||||
'robot_id': robot.robot_id,
|
||||
'type': robot.type,
|
||||
'name': robot.name
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取任务详情成功',
|
||||
'data': {
|
||||
'task_id': task.task_id,
|
||||
'name': task.name,
|
||||
'project': {
|
||||
'project_id': task.project.project_id,
|
||||
'name': task.project.name,
|
||||
'repository': task.project.repository
|
||||
} if task.project else None,
|
||||
'environment': {
|
||||
'environment_id': task.environment.environment_id,
|
||||
'name': task.environment.name,
|
||||
'type': task.environment.type
|
||||
} if task.environment else None,
|
||||
'description': task.description,
|
||||
'requirement': task.requirement,
|
||||
'branch': task.branch,
|
||||
'git_token': {
|
||||
'credential_id': task.git_token.credential_id,
|
||||
'name': task.git_token.name
|
||||
} if task.git_token else None,
|
||||
'stages': task.stages,
|
||||
'notification_channels': task.notification_channels,
|
||||
'notification_robots': notification_robots,
|
||||
# 外部脚本库配置
|
||||
'use_external_script': task.use_external_script,
|
||||
'external_script_repo_url': task.external_script_config.get('repo_url', '') if task.external_script_config else '',
|
||||
'external_script_directory': task.external_script_config.get('directory', '') if task.external_script_config else '',
|
||||
'external_script_branch': task.external_script_config.get('branch', '') if task.external_script_config else '',
|
||||
'external_script_token_id': task.external_script_config.get('token_id') if task.external_script_config else None,
|
||||
'status': task.status,
|
||||
'building_status': task.building_status, # 添加构建状态字段
|
||||
'version': task.version,
|
||||
'last_build_number': task.last_build_number,
|
||||
'total_builds': task.total_builds,
|
||||
'success_builds': task.success_builds,
|
||||
'failure_builds': task.failure_builds,
|
||||
'last_build': {
|
||||
'id': latest_build.history_id,
|
||||
'number': latest_build.build_number,
|
||||
'status': latest_build.status,
|
||||
'time': latest_build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': '未完成' if latest_build.status in ['pending', 'running'] else str(latest_build.build_time.get('total_duration', 0)) + '秒'
|
||||
} if latest_build else None,
|
||||
'creator': {
|
||||
'user_id': task.creator.user_id,
|
||||
'name': task.creator.name
|
||||
} if task.creator else None,
|
||||
'create_time': task.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': task.update_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
|
||||
# 获取查询参数
|
||||
project_id = request.GET.get('project_id')
|
||||
environment_id = request.GET.get('environment_id')
|
||||
name = request.GET.get('name')
|
||||
|
||||
# 构建查询条件
|
||||
query = Q()
|
||||
|
||||
# 应用项目权限过滤
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if not permitted_project_ids:
|
||||
# 如果设置了自定义项目权限但列表为空,意味着没有权限查看任何项目
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何项目的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取任务列表成功',
|
||||
'data': []
|
||||
})
|
||||
|
||||
# 用户只能查看有权限的项目
|
||||
if project_id and project_id != 'all':
|
||||
# 如果指定了项目,检查是否有该项目的权限
|
||||
if project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{project_id}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该项目的构建任务'
|
||||
}, status=403)
|
||||
query &= Q(project__project_id=project_id)
|
||||
else:
|
||||
# 如果没有指定项目或选择了全部,则限制为有权限的项目
|
||||
query &= Q(project__project_id__in=permitted_project_ids)
|
||||
else:
|
||||
# 如果有全部项目权限,并且指定了项目ID
|
||||
if project_id and project_id != 'all':
|
||||
query &= Q(project__project_id=project_id)
|
||||
|
||||
# 应用环境权限过滤
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if not permitted_environment_types:
|
||||
# 如果设置了自定义环境权限但列表为空,意味着没有权限查看任何环境
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何环境的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取任务列表成功',
|
||||
'data': []
|
||||
})
|
||||
|
||||
if environment_id and environment_id != 'all':
|
||||
# 如果指定了环境,需要检查是否在有权限的环境类型中
|
||||
try:
|
||||
env = Environment.objects.get(environment_id=environment_id)
|
||||
if env.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境[{environment_id}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境的构建任务'
|
||||
}, status=403)
|
||||
query &= Q(environment__environment_id=environment_id)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
}, status=404)
|
||||
else:
|
||||
# 如果没有指定环境或选择了全部,则限制为有权限的环境类型
|
||||
query &= Q(environment__type__in=permitted_environment_types)
|
||||
else:
|
||||
# 如果有全部环境权限,并且指定了环境ID
|
||||
if environment_id and environment_id != 'all':
|
||||
query &= Q(environment__environment_id=environment_id)
|
||||
|
||||
# 添加其他查询条件
|
||||
if name:
|
||||
query &= Q(name__icontains=name)
|
||||
|
||||
# 查询任务列表
|
||||
tasks = BuildTask.objects.select_related(
|
||||
'project',
|
||||
'environment',
|
||||
'creator'
|
||||
).filter(query)
|
||||
|
||||
task_list = []
|
||||
for task in tasks:
|
||||
# 获取最新的构建历史
|
||||
latest_build = BuildHistory.objects.filter(task=task).order_by('-build_number').first()
|
||||
|
||||
task_list.append({
|
||||
'task_id': task.task_id,
|
||||
'name': task.name,
|
||||
'project': {
|
||||
'project_id': task.project.project_id,
|
||||
'name': task.project.name
|
||||
} if task.project else None,
|
||||
'environment': {
|
||||
'environment_id': task.environment.environment_id,
|
||||
'name': task.environment.name,
|
||||
'type': task.environment.type
|
||||
} if task.environment else None,
|
||||
'description': task.description,
|
||||
'branch': task.branch,
|
||||
'status': task.status,
|
||||
'building_status': task.building_status, # 添加构建状态字段
|
||||
'version': task.version,
|
||||
'last_build_number': task.last_build_number,
|
||||
'total_builds': task.total_builds,
|
||||
'success_builds': task.success_builds,
|
||||
'failure_builds': task.failure_builds,
|
||||
'last_build': {
|
||||
'id': latest_build.history_id,
|
||||
'number': latest_build.build_number,
|
||||
'status': latest_build.status,
|
||||
'time': latest_build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': '未完成' if latest_build.status in ['pending', 'running'] else str(latest_build.build_time.get('total_duration', 0)) + '秒'
|
||||
} if latest_build else None,
|
||||
'creator': {
|
||||
'user_id': task.creator.user_id,
|
||||
'name': task.creator.name
|
||||
} if task.creator else None,
|
||||
'create_time': task.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': task.update_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取任务列表成功',
|
||||
'data': task_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取构建任务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建构建任务"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name')
|
||||
project_id = data.get('project_id')
|
||||
environment_id = data.get('environment_id')
|
||||
description = data.get('description')
|
||||
branch = data.get('branch', 'main')
|
||||
git_token_id = data.get('git_token_id')
|
||||
stages = data.get('stages', [])
|
||||
notification_channels = data.get('notification_channels', [])
|
||||
|
||||
# 外部脚本库配置
|
||||
use_external_script = data.get('use_external_script')
|
||||
external_script_config = None
|
||||
if 'use_external_script' in data:
|
||||
if use_external_script:
|
||||
repo_url = data.get('external_script_repo_url', '').strip()
|
||||
directory = data.get('external_script_directory', '').strip()
|
||||
external_script_branch = data.get('external_script_branch', '').strip()
|
||||
token_id = data.get('external_script_token_id')
|
||||
|
||||
# 验证外部脚本库必填字段
|
||||
if not repo_url:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库仓库地址不能为空'
|
||||
})
|
||||
if not directory:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库存放目录不能为空'
|
||||
})
|
||||
if not external_script_branch:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库分支名称不能为空'
|
||||
})
|
||||
|
||||
external_script_config = {
|
||||
'repo_url': repo_url,
|
||||
'directory': directory,
|
||||
'branch': external_script_branch,
|
||||
'token_id': token_id
|
||||
}
|
||||
else:
|
||||
external_script_config = {}
|
||||
|
||||
# 验证必要字段
|
||||
if not all([name, project_id, environment_id]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务名称、项目和环境不能为空'
|
||||
})
|
||||
|
||||
# 验证通知机器人是否存在
|
||||
if notification_channels:
|
||||
existing_robots = set(NotificationRobot.objects.filter(
|
||||
robot_id__in=notification_channels
|
||||
).values_list('robot_id', flat=True))
|
||||
invalid_robots = set(notification_channels) - existing_robots
|
||||
if invalid_robots:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': f'以下机器人不存在: {", ".join(invalid_robots)}'
|
||||
})
|
||||
|
||||
# 检查项目是否存在
|
||||
try:
|
||||
project = Project.objects.get(project_id=project_id)
|
||||
except Project.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '项目不存在'
|
||||
})
|
||||
|
||||
# 检查环境是否存在
|
||||
try:
|
||||
environment = Environment.objects.get(environment_id=environment_id)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
})
|
||||
|
||||
# 检查GitLab Token凭证是否存在
|
||||
git_token = None
|
||||
if git_token_id:
|
||||
try:
|
||||
git_token = GitlabTokenCredential.objects.get(credential_id=git_token_id)
|
||||
except GitlabTokenCredential.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': 'GitLab Token凭证不存在'
|
||||
})
|
||||
|
||||
# 创建构建任务
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
task = BuildTask.objects.create(
|
||||
task_id=generate_id(),
|
||||
name=name,
|
||||
project=project,
|
||||
environment=environment,
|
||||
description=description,
|
||||
branch=branch,
|
||||
git_token=git_token,
|
||||
stages=stages,
|
||||
notification_channels=notification_channels,
|
||||
use_external_script=use_external_script,
|
||||
external_script_config=external_script_config,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建构建任务成功',
|
||||
'data': {
|
||||
'task_id': task.task_id,
|
||||
'name': task.name
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建构建任务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""更新构建任务"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_permissions = function_permissions.get('build_task', [])
|
||||
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
|
||||
task_id = data.get('task_id')
|
||||
name = data.get('name')
|
||||
project_id = data.get('project_id')
|
||||
environment_id = data.get('environment_id')
|
||||
description = data.get('description')
|
||||
branch = data.get('branch')
|
||||
git_token_id = data.get('git_token_id')
|
||||
stages = data.get('stages')
|
||||
notification_channels = data.get('notification_channels')
|
||||
status = data.get('status')
|
||||
|
||||
# 外部脚本库配置
|
||||
use_external_script = data.get('use_external_script')
|
||||
external_script_config = None
|
||||
if 'use_external_script' in data:
|
||||
if use_external_script:
|
||||
repo_url = data.get('external_script_repo_url', '').strip()
|
||||
directory = data.get('external_script_directory', '').strip()
|
||||
external_script_branch = data.get('external_script_branch', '').strip()
|
||||
token_id = data.get('external_script_token_id')
|
||||
|
||||
# 验证外部脚本库必填字段
|
||||
if not repo_url:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库仓库地址不能为空'
|
||||
})
|
||||
if not directory:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库存放目录不能为空'
|
||||
})
|
||||
if not external_script_branch:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '外部脚本库分支名称不能为空'
|
||||
})
|
||||
|
||||
external_script_config = {
|
||||
'repo_url': repo_url,
|
||||
'directory': directory,
|
||||
'branch': external_script_branch,
|
||||
'token_id': token_id
|
||||
}
|
||||
else:
|
||||
external_script_config = {}
|
||||
|
||||
if not task_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
task = BuildTask.objects.get(task_id=task_id)
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
|
||||
# 如果只修改状态,需要检查是否有禁用权限
|
||||
if status and len(data) == 2 and 'task_id' in data and 'status' in data:
|
||||
if 'disable' not in build_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有禁用/启用任务权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限禁用/启用任务'
|
||||
}, status=403)
|
||||
else:
|
||||
# 否则检查是否有编辑权限
|
||||
if 'edit' not in build_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有编辑任务权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限编辑任务'
|
||||
}, status=403)
|
||||
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_id and project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试编辑无权限的项目[{project_id}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限编辑该项目的构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_id and environment_scope == 'custom':
|
||||
try:
|
||||
env = Environment.objects.get(environment_id=environment_id)
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if env.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试编辑无权限的环境类型[{env.type}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限编辑该环境的构建任务'
|
||||
}, status=403)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
})
|
||||
|
||||
# 更新项目关联
|
||||
if project_id:
|
||||
try:
|
||||
project = Project.objects.get(project_id=project_id)
|
||||
task.project = project
|
||||
except Project.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '项目不存在'
|
||||
})
|
||||
|
||||
# 更新环境关联
|
||||
if environment_id:
|
||||
try:
|
||||
environment = Environment.objects.get(environment_id=environment_id)
|
||||
task.environment = environment
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
})
|
||||
|
||||
# 更新GitLab Token凭证关联
|
||||
if 'git_token_id' in data:
|
||||
if git_token_id:
|
||||
try:
|
||||
git_token = GitlabTokenCredential.objects.get(credential_id=git_token_id)
|
||||
task.git_token = git_token
|
||||
except GitlabTokenCredential.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': 'GitLab Token凭证不存在'
|
||||
})
|
||||
else:
|
||||
task.git_token = None
|
||||
|
||||
# 更新其他字段
|
||||
if 'name' in data:
|
||||
task.name = name
|
||||
if 'description' in data:
|
||||
task.description = description
|
||||
if 'branch' in data:
|
||||
task.branch = branch
|
||||
if 'stages' in data:
|
||||
task.stages = stages
|
||||
if 'notification_channels' in data:
|
||||
# 验证通知机器人是否存在
|
||||
existing_robots = set(NotificationRobot.objects.filter(
|
||||
robot_id__in=notification_channels
|
||||
).values_list('robot_id', flat=True))
|
||||
invalid_robots = set(notification_channels) - existing_robots
|
||||
if invalid_robots:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': f'以下机器人不存在: {", ".join(invalid_robots)}'
|
||||
})
|
||||
task.notification_channels = notification_channels
|
||||
if 'status' in data:
|
||||
task.status = status
|
||||
|
||||
# 更新外部脚本库配置
|
||||
if 'use_external_script' in data:
|
||||
task.use_external_script = use_external_script
|
||||
task.external_script_config = external_script_config
|
||||
|
||||
task.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新构建任务成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新构建任务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除构建任务"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
task_id = data.get('task_id')
|
||||
|
||||
if not task_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
task = BuildTask.objects.get(task_id=task_id)
|
||||
task.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除构建任务成功'
|
||||
})
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'删除构建任务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildExecuteView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""执行构建"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有执行构建权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_permissions = function_permissions.get('build_task', [])
|
||||
|
||||
if 'execute' not in build_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有执行构建权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限执行构建任务'
|
||||
}, status=403)
|
||||
|
||||
data = json.loads(request.body)
|
||||
task_id = data.get('task_id')
|
||||
branch = data.get('branch') # 获取用户选择的分支
|
||||
commit_id = data.get('commit_id')
|
||||
version = data.get('version')
|
||||
requirement = data.get('requirement')
|
||||
|
||||
if not task_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
task = BuildTask.objects.select_related('project', 'environment').get(task_id=task_id)
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
|
||||
# 检查是否有权限执行该任务
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if task.project and task.project.project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试执行无权限的项目[{task.project.project_id}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限执行该项目的构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if task.environment and task.environment.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试执行无权限的环境类型[{task.environment.type}]的构建任务')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限执行该环境的构建任务'
|
||||
}, status=403)
|
||||
|
||||
# 根据环境类型检查必要参数
|
||||
env_type = task.environment.type if task.environment else None
|
||||
|
||||
if env_type in ['development', 'testing']:
|
||||
# 开发环境和测试环境需要分支和commit_id
|
||||
if not branch:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '分支不能为空'
|
||||
})
|
||||
|
||||
if not commit_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': 'Commit ID不能为空'
|
||||
})
|
||||
elif env_type in ['staging', 'production']:
|
||||
if not version:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '版本号不能为空'
|
||||
})
|
||||
parts = version.split('_')
|
||||
if len(parts) == 2 and len(parts[1]) >= 8:
|
||||
commit_id = parts[1]
|
||||
else:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '版本号格式不正确,应为:YYYYMMDDHHmmSS_commitId'
|
||||
})
|
||||
|
||||
if not requirement:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '构建需求描述不能为空'
|
||||
})
|
||||
|
||||
# 检查任务的构建状态
|
||||
if task.building_status == 'building':
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '当前任务正在构建中,请等待构建完成后再试'
|
||||
})
|
||||
|
||||
running_build = BuildHistory.objects.filter(
|
||||
task_id=task_id,
|
||||
status__in=['pending', 'running']
|
||||
).exists()
|
||||
|
||||
if running_build:
|
||||
# 如果有正在进行的构建,但building_status不是building,则修正状态
|
||||
BuildTask.objects.filter(task_id=task_id).update(building_status='building')
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '当前任务有正在进行的构建,请等待构建完成后再试'
|
||||
})
|
||||
|
||||
if task.status == 'disabled':
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务已禁用'
|
||||
})
|
||||
|
||||
# 生成构建号
|
||||
build_number = task.last_build_number + 1
|
||||
|
||||
# 创建构建历史记录
|
||||
history = BuildHistory.objects.create(
|
||||
history_id=generate_id(),
|
||||
task=task,
|
||||
build_number=build_number,
|
||||
branch=branch if branch else '', # 对于预发布和生产环境,分支可能为空
|
||||
commit_id=commit_id,
|
||||
version=version if version else None, # 对于预发布和生产环境,使用传入的版本号
|
||||
status='pending', # 初始状态为等待中
|
||||
requirement=requirement,
|
||||
operator=User.objects.get(user_id=request.user_id) # 记录构建人
|
||||
)
|
||||
|
||||
# 更新任务状态、构建号和构建状态
|
||||
BuildTask.objects.filter(task_id=task_id).update(
|
||||
last_build_number=build_number,
|
||||
total_builds=F('total_builds') + 1,
|
||||
building_status='building' # 设置为构建中状态
|
||||
)
|
||||
|
||||
# 在新线程中执行构建
|
||||
build_thread = threading.Thread(
|
||||
target=execute_build,
|
||||
args=(task, build_number, commit_id, history)
|
||||
)
|
||||
build_thread.start()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '开始构建',
|
||||
'data': {
|
||||
'build_number': build_number,
|
||||
'history_id': history.history_id
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'执行构建失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""停止构建"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
history_id = data.get('history_id')
|
||||
|
||||
if not history_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '历史ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
history = BuildHistory.objects.get(history_id=history_id)
|
||||
except BuildHistory.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '构建历史不存在'
|
||||
})
|
||||
|
||||
# 只有进行中的构建可以停止
|
||||
if history.status not in ['pending', 'running']:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '只能停止进行中的构建'
|
||||
})
|
||||
|
||||
# 更新构建状态为terminated
|
||||
history.status = 'terminated'
|
||||
|
||||
# 如果构建日志存在,追加终止消息
|
||||
if history.build_log:
|
||||
history.build_log += "\n[系统] 构建被手动终止\n"
|
||||
else:
|
||||
history.build_log = "[系统] 构建被手动终止\n"
|
||||
|
||||
# 更新构建时间
|
||||
if not history.build_time:
|
||||
history.build_time = {}
|
||||
|
||||
if 'start_time' in history.build_time and 'total_duration' not in history.build_time:
|
||||
# 计算从开始到现在的持续时间
|
||||
start_time = datetime.strptime(history.build_time['start_time'], '%Y-%m-%d %H:%M:%S')
|
||||
duration = int((datetime.now() - start_time).total_seconds())
|
||||
history.build_time['total_duration'] = str(duration)
|
||||
history.build_time['end_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
history.save()
|
||||
|
||||
# 更新任务统计信息和构建状态
|
||||
BuildTask.objects.filter(task_id=history.task.task_id).update(
|
||||
building_status='idle' # 重置构建状态为空闲
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '构建已终止'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'停止构建失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
549
backend/apps/views/build_history.py
Normal file
549
backend/apps/views/build_history.py
Normal file
@@ -0,0 +1,549 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db.models import Q
|
||||
from ..models import BuildHistory, BuildTask, Project, Environment
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.permissions import get_user_permissions
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildHistoryView(View):
|
||||
def _get_stage_status_from_log(self, log: str, stage_name: str, overall_status: str = None) -> str:
|
||||
"""从日志中获取指定阶段的状态"""
|
||||
if not log:
|
||||
return 'failed'
|
||||
|
||||
# 处理特殊阶段:Git Clone
|
||||
if stage_name == 'Git Clone':
|
||||
# Git Clone 阶段使用 [Git Clone] 格式
|
||||
if '[Git Clone]' not in log:
|
||||
return 'failed'
|
||||
|
||||
# 检查是否有完成标记
|
||||
if '[Git Clone] 代码克隆完成' in log:
|
||||
return 'success'
|
||||
elif overall_status == 'terminated':
|
||||
return 'terminated'
|
||||
else:
|
||||
return 'failed'
|
||||
|
||||
# 处理普通构建阶段
|
||||
stage_start_pattern = f'[Build Stages] 开始执行阶段: {stage_name}'
|
||||
stage_complete_pattern = f'[Build Stages] 阶段 {stage_name} 执行完成'
|
||||
|
||||
# 如果整体构建状态是terminated,检查阶段是否有开始执行
|
||||
if overall_status == 'terminated':
|
||||
if stage_start_pattern in log:
|
||||
# 检查阶段是否完成
|
||||
if stage_complete_pattern in log:
|
||||
return 'success'
|
||||
else:
|
||||
return 'terminated'
|
||||
else:
|
||||
# 没有该阶段的日志,说明还没开始执行就被终止了
|
||||
return 'terminated'
|
||||
|
||||
if stage_start_pattern not in log:
|
||||
return 'failed' # 阶段未开始执行
|
||||
|
||||
# 检查阶段是否完成
|
||||
if stage_complete_pattern in log:
|
||||
return 'success' # 阶段成功完成
|
||||
else:
|
||||
return 'failed' # 阶段开始了但没有完成
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取构建历史列表"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有构建历史查看权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_history_permissions = function_permissions.get('build_history', [])
|
||||
|
||||
if 'view' not in build_history_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有构建历史查看权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看构建历史'
|
||||
}, status=403)
|
||||
|
||||
# 获取查询参数
|
||||
project_id = request.GET.get('project_id')
|
||||
environment_id = request.GET.get('environment_id')
|
||||
task_id = request.GET.get('task_id') # 添加task_id参数
|
||||
task_name = request.GET.get('task_name')
|
||||
page = int(request.GET.get('page', 1))
|
||||
page_size = int(request.GET.get('page_size', 10))
|
||||
|
||||
# 构建查询条件
|
||||
query = Q()
|
||||
|
||||
# 应用项目权限过滤
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if not permitted_project_ids:
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何项目的构建历史')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建历史列表成功',
|
||||
'data': [],
|
||||
'total': 0,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
})
|
||||
|
||||
# 用户只能查看有权限的项目
|
||||
if project_id and project_id != 'all':
|
||||
# 如果指定了项目,检查是否有该项目的权限
|
||||
if project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{project_id}]的构建历史')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该项目的构建历史'
|
||||
}, status=403)
|
||||
query &= Q(task__project__project_id=project_id)
|
||||
else:
|
||||
query &= Q(task__project__project_id__in=permitted_project_ids)
|
||||
else:
|
||||
# 如果有全部项目权限,并且指定了项目ID
|
||||
if project_id and project_id != 'all':
|
||||
query &= Q(task__project__project_id=project_id)
|
||||
|
||||
# 应用环境权限过滤
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if not permitted_environment_types:
|
||||
# 如果设置了自定义环境权限但列表为空,意味着没有权限查看任何环境
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何环境的构建历史')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建历史列表成功',
|
||||
'data': [],
|
||||
'total': 0,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
})
|
||||
|
||||
if environment_id and environment_id != 'all':
|
||||
# 如果指定了环境,需要检查是否在有权限的环境类型中
|
||||
try:
|
||||
env = Environment.objects.get(environment_id=environment_id)
|
||||
if env.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境[{environment_id}]的构建历史')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境的构建历史'
|
||||
}, status=403)
|
||||
query &= Q(task__environment__environment_id=environment_id)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
}, status=404)
|
||||
else:
|
||||
# 如果没有指定环境或选择了全部,则限制为有权限的环境类型
|
||||
query &= Q(task__environment__type__in=permitted_environment_types)
|
||||
else:
|
||||
# 如果有全部环境权限,并且指定了环境ID
|
||||
if environment_id and environment_id != 'all':
|
||||
query &= Q(task__environment__environment_id=environment_id)
|
||||
|
||||
# 添加其他查询条件
|
||||
if task_id:
|
||||
query &= Q(task__task_id=task_id)
|
||||
if task_name:
|
||||
query &= Q(task__name__icontains=task_name)
|
||||
|
||||
# 查询构建历史
|
||||
histories = BuildHistory.objects.select_related(
|
||||
'task',
|
||||
'task__project',
|
||||
'task__environment',
|
||||
'operator'
|
||||
).filter(query).order_by('-create_time')
|
||||
|
||||
# 计算总数
|
||||
total = histories.count()
|
||||
|
||||
# 分页
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
histories = histories[start:end]
|
||||
|
||||
# 构建返回数据
|
||||
history_list = []
|
||||
for history in histories:
|
||||
# 计算构建耗时
|
||||
duration = '未完成'
|
||||
if history.build_time and 'total_duration' in history.build_time:
|
||||
duration_seconds = int(history.build_time['total_duration'])
|
||||
if duration_seconds < 60:
|
||||
duration = f"{duration_seconds}秒"
|
||||
else:
|
||||
minutes = duration_seconds // 60
|
||||
seconds = duration_seconds % 60
|
||||
duration = f"{minutes}分{seconds}秒"
|
||||
|
||||
# 处理构建阶段信息
|
||||
stages = []
|
||||
|
||||
# 添加 Git Clone 阶段
|
||||
git_clone_stage = next(
|
||||
(t for t in history.build_time.get('stages_time', []) if t['name'] == 'Git Clone'),
|
||||
None
|
||||
) if history.build_time else None
|
||||
|
||||
if git_clone_stage:
|
||||
git_clone_status = self._get_stage_status_from_log(history.build_log, 'Git Clone', history.status)
|
||||
stages.append({
|
||||
'name': 'Git Clone',
|
||||
'status': git_clone_status,
|
||||
'startTime': git_clone_stage['start_time'],
|
||||
'duration': git_clone_stage['duration'] + '秒'
|
||||
})
|
||||
|
||||
# 添加其他阶段
|
||||
for stage in history.stages:
|
||||
stage_time = next(
|
||||
(t for t in history.build_time.get('stages_time', []) if t['name'] == stage['name']),
|
||||
None
|
||||
) if history.build_time else None
|
||||
|
||||
stage_status = self._get_stage_status_from_log(history.build_log, stage['name'], history.status)
|
||||
stages.append({
|
||||
'name': stage['name'],
|
||||
'status': stage_status,
|
||||
'startTime': stage_time['start_time'] if stage_time else None,
|
||||
'duration': stage_time['duration'] + '秒' if stage_time else '未知'
|
||||
})
|
||||
|
||||
# 检查是否有回滚权限
|
||||
can_rollback = history.status == 'success'
|
||||
|
||||
history_list.append({
|
||||
'id': history.history_id,
|
||||
'build_number': history.build_number,
|
||||
'status': history.status,
|
||||
'branch': history.branch,
|
||||
'commit': history.commit_id[:8] if history.commit_id else None,
|
||||
'version': history.version,
|
||||
'environment': history.task.environment.name if history.task.environment else None,
|
||||
'startTime': history.build_time.get('start_time') if history.build_time else history.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': duration,
|
||||
'operator': history.operator.name if history.operator else None,
|
||||
'requirement': history.requirement,
|
||||
'stages': stages,
|
||||
'canRollback': can_rollback,
|
||||
'task': {
|
||||
'id': history.task.task_id,
|
||||
'name': history.task.name,
|
||||
'description': history.task.description
|
||||
}
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建历史列表成功',
|
||||
'data': history_list,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取构建历史列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""回滚到指定版本"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_history_permissions = function_permissions.get('build_history', [])
|
||||
|
||||
# 检查是否有回滚权限
|
||||
if 'rollback' not in build_history_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有构建历史回滚权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限执行回滚操作'
|
||||
}, status=403)
|
||||
|
||||
data = json.loads(request.body)
|
||||
history_id = data.get('history_id')
|
||||
|
||||
if not history_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '历史ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
|
||||
except BuildHistory.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '构建历史不存在'
|
||||
})
|
||||
|
||||
# 检查项目和环境权限
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if history.task.project.project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试回滚无权限的项目[{history.task.project.project_id}]的构建')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限回滚该项目的构建'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if history.task.environment.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试回滚无权限的环境类型[{history.task.environment.type}]的构建')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限回滚该环境的构建'
|
||||
}, status=403)
|
||||
|
||||
if history.status != 'success':
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '只能回滚到构建成功的版本'
|
||||
})
|
||||
|
||||
# TODO: 实现回滚逻辑
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '开始回滚',
|
||||
'data': {
|
||||
'version': history.version
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'回滚失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildLogView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request, history_id):
|
||||
"""获取构建日志"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有构建历史查看日志权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_task_permissions = function_permissions.get('build_task', [])
|
||||
build_history_permissions = function_permissions.get('build_history', [])
|
||||
|
||||
has_log_permission = 'view_log' in build_task_permissions or 'view_log' in build_history_permissions
|
||||
|
||||
if not has_log_permission:
|
||||
logger.warning(f'用户[{request.user_id}]没有构建历史查看日志权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看构建日志'
|
||||
}, status=403)
|
||||
|
||||
try:
|
||||
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
|
||||
except BuildHistory.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '构建历史不存在'
|
||||
})
|
||||
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if history.task.project and history.task.project.project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{history.task.project.project_id}]的构建日志')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该项目的构建日志'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if history.task.environment and history.task.environment.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{history.task.environment.type}]的构建日志')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境的构建日志'
|
||||
}, status=403)
|
||||
|
||||
# 检查是否为下载请求
|
||||
is_download = request.GET.get('download') == 'true'
|
||||
if is_download:
|
||||
# 生成日志文件名
|
||||
filename = f"build_log_{history.task.name}_{history.build_number}.txt"
|
||||
|
||||
# 准备日志内容
|
||||
log_content = history.build_log or '暂无日志'
|
||||
|
||||
# 创建响应对象
|
||||
response = HttpResponse(log_content, content_type='text/plain')
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建日志成功',
|
||||
'data': {
|
||||
'log': history.build_log or '暂无日志'
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取构建日志失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildStageLogView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request, history_id, stage_name):
|
||||
"""获取构建阶段日志"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有构建历史查看日志权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
build_task_permissions = function_permissions.get('build_task', [])
|
||||
build_history_permissions = function_permissions.get('build_history', [])
|
||||
|
||||
# 只要有任何一方的view_log权限即可
|
||||
has_log_permission = 'view_log' in build_task_permissions or 'view_log' in build_history_permissions
|
||||
|
||||
if not has_log_permission:
|
||||
logger.warning(f'用户[{request.user_id}]没有构建历史查看日志权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看构建日志'
|
||||
}, status=403)
|
||||
|
||||
try:
|
||||
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
|
||||
except BuildHistory.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '构建历史不存在'
|
||||
})
|
||||
|
||||
# 项目权限检查
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if history.task.project and history.task.project.project_id not in permitted_project_ids:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{history.task.project.project_id}]的构建阶段日志')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该项目的构建日志'
|
||||
}, status=403)
|
||||
|
||||
# 环境权限检查
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if history.task.environment and history.task.environment.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{history.task.environment.type}]的构建阶段日志')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境的构建日志'
|
||||
}, status=403)
|
||||
|
||||
# 在完整日志中查找指定阶段的日志
|
||||
if not history.build_log:
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取阶段日志成功',
|
||||
'data': {
|
||||
'log': '暂无日志'
|
||||
}
|
||||
})
|
||||
|
||||
# 适配Jenkins风格日志格式的阶段日志解析
|
||||
stage_logs = []
|
||||
lines = history.build_log.split('\n')
|
||||
in_stage = False
|
||||
|
||||
# 处理特殊阶段:Git Clone
|
||||
if stage_name == 'Git Clone':
|
||||
# Git Clone 阶段使用 [Git Clone] 格式
|
||||
for line in lines:
|
||||
if '[Git Clone]' in line:
|
||||
stage_logs.append(line)
|
||||
else:
|
||||
# 普通构建阶段使用 [Build Stages] 格式
|
||||
stage_start_pattern = f'[Build Stages] 开始执行阶段: {stage_name}'
|
||||
stage_complete_pattern = f'[Build Stages] 阶段 {stage_name} 执行完成'
|
||||
|
||||
for line in lines:
|
||||
if stage_start_pattern in line:
|
||||
in_stage = True
|
||||
stage_logs.append(line)
|
||||
elif in_stage and stage_complete_pattern in line:
|
||||
stage_logs.append(line)
|
||||
break # 阶段结束
|
||||
elif in_stage:
|
||||
# 在阶段执行期间的所有日志都属于该阶段
|
||||
# 排除其他阶段的开始标记
|
||||
if '[Build Stages] 开始执行阶段:' not in line:
|
||||
stage_logs.append(line)
|
||||
else:
|
||||
# 遇到其他阶段开始,当前阶段结束
|
||||
break
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取阶段日志成功',
|
||||
'data': {
|
||||
'log': '\n'.join(stage_logs) if stage_logs else '暂无该阶段日志'
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取阶段日志失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
273
backend/apps/views/build_sse.py
Normal file
273
backend/apps/views/build_sse.py
Normal file
@@ -0,0 +1,273 @@
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import uuid
|
||||
import threading
|
||||
import queue
|
||||
import asyncio
|
||||
from django.http import StreamingHttpResponse, JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from asgiref.sync import sync_to_async
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.log_stream import log_stream_manager
|
||||
from ..models import BuildHistory, UserToken
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildLogSSEView(View):
|
||||
"""构建日志SSE流视图 - 异步实现以支持ASGI环境"""
|
||||
|
||||
def options(self, request, task_id, build_number):
|
||||
"""处理CORS预检请求"""
|
||||
response = JsonResponse({})
|
||||
response['Access-Control-Allow-Origin'] = '*'
|
||||
response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
|
||||
response['Access-Control-Allow-Headers'] = 'Cache-Control, Authorization'
|
||||
response['Access-Control-Max-Age'] = '86400' # 24小时
|
||||
return response
|
||||
|
||||
def get(self, request, task_id, build_number):
|
||||
"""获取构建日志SSE流"""
|
||||
try:
|
||||
# 从URL参数获取token进行认证
|
||||
token = request.GET.get('token')
|
||||
if not token:
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream("缺少认证token"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
|
||||
# 验证token
|
||||
try:
|
||||
user_info = self._verify_jwt_token(token)
|
||||
if not user_info:
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream("无效的认证token"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Token验证失败: {str(e)}", exc_info=True)
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream("认证失败"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
|
||||
# 验证构建历史记录是否存在
|
||||
try:
|
||||
history = BuildHistory.objects.get(
|
||||
task__task_id=task_id,
|
||||
build_number=int(build_number)
|
||||
)
|
||||
except BuildHistory.DoesNotExist:
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream("构建记录不存在"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
except ValueError:
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream("无效的构建号"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
|
||||
# 创建异步SSE流
|
||||
response = StreamingHttpResponse(
|
||||
self._build_log_stream_async(task_id, int(build_number), history),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
|
||||
# 设置SSE相关的HTTP头
|
||||
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response['Pragma'] = 'no-cache'
|
||||
response['Expires'] = '0'
|
||||
response['Access-Control-Allow-Origin'] = '*'
|
||||
response['Access-Control-Allow-Headers'] = 'Cache-Control'
|
||||
response['Access-Control-Allow-Methods'] = 'GET'
|
||||
response['X-Accel-Buffering'] = 'no' # 禁用nginx缓冲
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建SSE流失败: {str(e)}", exc_info=True)
|
||||
return StreamingHttpResponse(
|
||||
self._create_error_stream(f"服务器错误: {str(e)}"),
|
||||
content_type='text/event-stream'
|
||||
)
|
||||
|
||||
def _verify_jwt_token(self, token):
|
||||
"""验证JWT token"""
|
||||
try:
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
|
||||
# 查询用户token
|
||||
user_token = UserToken.objects.filter(token=token).first()
|
||||
|
||||
if not user_token:
|
||||
logger.warning("Token不存在于数据库中")
|
||||
return None
|
||||
|
||||
# 验证JWT token
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
return {
|
||||
'user_id': payload.get('user_id'),
|
||||
'username': payload.get('username')
|
||||
}
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("Token已过期")
|
||||
return None
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning(f"Token无效: {str(e)}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Token验证过程发生错误: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def _build_log_stream_async(self, task_id, build_number, history):
|
||||
"""异步生成构建日志流"""
|
||||
# 生成唯一的客户端ID
|
||||
client_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
# 发送连接建立消息
|
||||
yield self._format_sse_message({
|
||||
'type': 'connection_established',
|
||||
'message': '连接成功,开始接收构建日志...'
|
||||
})
|
||||
|
||||
# 如果构建已完成,发送完成消息并结束
|
||||
if history.status in ['success', 'failed', 'terminated']:
|
||||
yield self._format_sse_message({
|
||||
'type': 'build_complete',
|
||||
'status': history.status,
|
||||
'message': f'构建已完成,状态: {history.status}。请使用历史日志API获取完整日志。'
|
||||
})
|
||||
return
|
||||
|
||||
# 对于正在进行的构建,使用实时日志流
|
||||
heartbeat_counter = 0
|
||||
|
||||
# 获取异步日志流
|
||||
async for log_msg in self._async_log_stream(task_id, build_number, client_id):
|
||||
if log_msg is None:
|
||||
# 心跳包
|
||||
heartbeat_counter += 1
|
||||
if heartbeat_counter >= 30: # 每30秒发送一次心跳
|
||||
yield self._format_sse_message({
|
||||
'type': 'heartbeat',
|
||||
'timestamp': int(time.time())
|
||||
}, event_type='heartbeat')
|
||||
heartbeat_counter = 0
|
||||
continue
|
||||
|
||||
# 重置心跳计数器
|
||||
heartbeat_counter = 0
|
||||
|
||||
# 检查是否是构建完成消息
|
||||
if log_msg.message.startswith('BUILD_COMPLETE:'):
|
||||
status = log_msg.message.split(':', 1)[1]
|
||||
yield self._format_sse_message({
|
||||
'type': 'build_complete',
|
||||
'status': status,
|
||||
'message': f'构建已完成,状态: {status}'
|
||||
})
|
||||
break
|
||||
else:
|
||||
# 普通日志消息
|
||||
yield self._format_sse_message({
|
||||
'type': 'build_log',
|
||||
'message': log_msg.message
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成构建日志流时发生错误: {str(e)}", exc_info=True)
|
||||
yield self._format_sse_message({
|
||||
'type': 'error',
|
||||
'message': f'日志流发生错误: {str(e)}'
|
||||
})
|
||||
|
||||
async def _async_log_stream(self, task_id, build_number, client_id):
|
||||
"""异步日志流生成器 - 改进版本"""
|
||||
try:
|
||||
async_queue = asyncio.Queue(maxsize=1000) # 限制队列大小
|
||||
stop_event = asyncio.Event()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def sync_log_reader():
|
||||
"""在单独线程中读取同步日志流"""
|
||||
try:
|
||||
for log_msg in log_stream_manager.get_log_stream(task_id, build_number, client_id):
|
||||
if stop_event.is_set():
|
||||
break
|
||||
|
||||
# 使用线程安全的方式添加到异步队列
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
async_queue.put(log_msg), loop
|
||||
).result(timeout=0.1)
|
||||
except Exception as queue_error:
|
||||
logger.debug(f"队列添加失败,可能客户端已断开: {queue_error}")
|
||||
break
|
||||
|
||||
# 发送结束信号
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
async_queue.put(StopAsyncIteration), loop
|
||||
).result(timeout=0.1)
|
||||
except Exception:
|
||||
pass # 忽略结束信号发送失败
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步日志读取器出错: {str(e)}", exc_info=True)
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
async_queue.put(StopAsyncIteration), loop
|
||||
).result(timeout=0.1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 在线程池中启动同步日志读取器
|
||||
thread = threading.Thread(target=sync_log_reader, daemon=True)
|
||||
thread.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
# 异步等待日志消息,使用较短的超时以提供更好的响应性
|
||||
log_msg = await asyncio.wait_for(async_queue.get(), timeout=1.0)
|
||||
|
||||
if log_msg is StopAsyncIteration:
|
||||
break
|
||||
|
||||
yield log_msg
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 超时,发送心跳
|
||||
yield None
|
||||
|
||||
finally:
|
||||
# 设置停止事件,清理资源
|
||||
stop_event.set()
|
||||
if thread.is_alive():
|
||||
thread.join(timeout=2.0) # 增加join超时时间
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"异步日志流错误: {str(e)}", exc_info=True)
|
||||
yield None # 确保生成器正常结束
|
||||
|
||||
def _create_error_stream(self, error_message):
|
||||
"""创建错误流"""
|
||||
yield self._format_sse_message({
|
||||
'type': 'error',
|
||||
'message': error_message
|
||||
})
|
||||
|
||||
def _format_sse_message(self, data, event_type='message'):
|
||||
"""格式化SSE消息"""
|
||||
message = f"event: {event_type}\n"
|
||||
message += f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
return message
|
||||
1044
backend/apps/views/credentials.py
Normal file
1044
backend/apps/views/credentials.py
Normal file
File diff suppressed because it is too large
Load Diff
288
backend/apps/views/dashboard.py
Normal file
288
backend/apps/views/dashboard.py
Normal file
@@ -0,0 +1,288 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db.models import Count
|
||||
from ..models import Project, BuildTask, BuildHistory, User, Environment
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class DashboardStatsView(View):
|
||||
"""首页统计数据接口"""
|
||||
|
||||
def get(self, request):
|
||||
"""获取首页统计数据"""
|
||||
try:
|
||||
# 获取项目总数
|
||||
project_count = Project.objects.count()
|
||||
|
||||
# 获取构建任务总数
|
||||
task_count = BuildTask.objects.count()
|
||||
|
||||
# 获取用户总数
|
||||
user_count = User.objects.count()
|
||||
|
||||
# 获取环境总数
|
||||
env_count = Environment.objects.count()
|
||||
|
||||
# 获取总构建数量
|
||||
total_builds_count = BuildHistory.objects.count()
|
||||
|
||||
# 获取最近7天的构建成功率
|
||||
seven_days_ago = datetime.now() - timedelta(days=7)
|
||||
recent_builds = BuildHistory.objects.filter(create_time__gte=seven_days_ago)
|
||||
total_recent_builds = recent_builds.count()
|
||||
success_recent_builds = recent_builds.filter(status='success').count()
|
||||
|
||||
success_rate = 0
|
||||
if total_recent_builds > 0:
|
||||
success_rate = round((success_recent_builds / total_recent_builds) * 100, 2)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取首页统计数据成功',
|
||||
'data': {
|
||||
'project_count': project_count,
|
||||
'task_count': task_count,
|
||||
'user_count': user_count,
|
||||
'env_count': env_count,
|
||||
'total_builds_count': total_builds_count,
|
||||
'success_rate': success_rate,
|
||||
'total_recent_builds': total_recent_builds
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取首页统计数据失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildTrendView(View):
|
||||
"""构建任务趋势接口"""
|
||||
|
||||
def get(self, request):
|
||||
"""获取构建任务趋势数据"""
|
||||
try:
|
||||
# 获取时间范围参数,默认为最近7天
|
||||
days = int(request.GET.get('days', 7))
|
||||
|
||||
# 计算日期范围:包含今天在内的最近 days 天
|
||||
today = datetime.now().date() # 获取今天的日期部分
|
||||
start_date = today - timedelta(days=days - 1) # 开始日期是今天往前 days-1 天
|
||||
|
||||
# 准备日期列表和结果数据
|
||||
date_list = []
|
||||
success_data = []
|
||||
failed_data = []
|
||||
|
||||
# 生成从 start_date 到 today 的日期列表
|
||||
current_date = start_date
|
||||
while current_date <= today:
|
||||
date_str = current_date.strftime('%Y-%m-%d')
|
||||
date_list.append(date_str)
|
||||
|
||||
# 查询当天的构建数据
|
||||
day_start = datetime.combine(current_date, datetime.min.time())
|
||||
day_end = datetime.combine(current_date, datetime.max.time())
|
||||
|
||||
# 成功构建数
|
||||
success_count = BuildHistory.objects.filter(
|
||||
create_time__gte=day_start,
|
||||
create_time__lte=day_end,
|
||||
status='success'
|
||||
).count()
|
||||
|
||||
# 失败构建数
|
||||
failed_count = BuildHistory.objects.filter(
|
||||
create_time__gte=day_start,
|
||||
create_time__lte=day_end,
|
||||
status='failed'
|
||||
).count()
|
||||
|
||||
success_data.append(success_count)
|
||||
failed_data.append(failed_count)
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建任务趋势数据成功',
|
||||
'data': {
|
||||
'dates': date_list,
|
||||
'success': success_data,
|
||||
'failed': failed_data
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取构建任务趋势数据失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BuildDetailView(View):
|
||||
"""构建详细数据接口"""
|
||||
|
||||
def get(self, request):
|
||||
"""获取指定日期的构建详细数据"""
|
||||
try:
|
||||
# 获取日期参数
|
||||
date_str = request.GET.get('date')
|
||||
if not date_str:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '日期参数不能为空'
|
||||
})
|
||||
|
||||
# 解析日期
|
||||
try:
|
||||
date = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
day_start = datetime(date.year, date.month, date.day, 0, 0, 0)
|
||||
day_end = datetime(date.year, date.month, date.day, 23, 59, 59)
|
||||
except ValueError:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '日期格式不正确,应为YYYY-MM-DD'
|
||||
})
|
||||
|
||||
# 查询当天的构建历史
|
||||
builds = BuildHistory.objects.filter(
|
||||
create_time__gte=day_start,
|
||||
create_time__lte=day_end
|
||||
).select_related('task', 'operator').order_by('-create_time')
|
||||
|
||||
build_list = []
|
||||
for build in builds:
|
||||
# 计算构建耗时
|
||||
duration = '未完成'
|
||||
if build.build_time and 'total_duration' in build.build_time:
|
||||
total_seconds = int(build.build_time.get('total_duration', 0))
|
||||
minutes = total_seconds // 60
|
||||
seconds = total_seconds % 60
|
||||
duration = f"{minutes}分{seconds}秒"
|
||||
|
||||
build_list.append({
|
||||
'id': build.history_id,
|
||||
'build_number': build.build_number,
|
||||
'task_name': build.task.name,
|
||||
'status': build.status,
|
||||
'branch': build.branch,
|
||||
'version': build.version,
|
||||
'start_time': build.build_time.get('start_time') if build.build_time else build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': duration,
|
||||
'operator': build.operator.name if build.operator else '系统'
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取构建详细数据成功',
|
||||
'data': build_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取构建详细数据失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class RecentBuildsView(View):
|
||||
"""最近构建任务接口"""
|
||||
|
||||
def get(self, request):
|
||||
"""获取最近构建任务数据"""
|
||||
try:
|
||||
# 获取数量参数,默认为10条
|
||||
limit = int(request.GET.get('limit', 5))
|
||||
|
||||
# 查询最近的构建历史
|
||||
recent_builds = BuildHistory.objects.select_related(
|
||||
'task', 'task__environment', 'operator' # 关联环境信息
|
||||
).order_by('-create_time')[:limit]
|
||||
|
||||
build_list = []
|
||||
for build in recent_builds:
|
||||
# 计算构建耗时
|
||||
duration = '未完成'
|
||||
if build.build_time and 'total_duration' in build.build_time:
|
||||
total_seconds = int(build.build_time.get('total_duration', 0))
|
||||
minutes = total_seconds // 60
|
||||
seconds = total_seconds % 60
|
||||
duration = f"{minutes}分 {seconds}秒"
|
||||
|
||||
build_list.append({
|
||||
'id': build.history_id,
|
||||
'build_number': build.build_number,
|
||||
'task_name': build.task.name,
|
||||
'status': build.status,
|
||||
'branch': build.branch,
|
||||
'version': build.version,
|
||||
'environment': build.task.environment.name if build.task.environment else None, # 添加环境名称
|
||||
'requirement': build.requirement,
|
||||
'start_time': build.build_time.get('start_time') if build.build_time else build.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'duration': duration,
|
||||
'operator': build.operator.name if build.operator else '系统'
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取最近构建任务数据成功',
|
||||
'data': build_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取最近构建任务数据失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class ProjectDistributionView(View):
|
||||
"""项目分布接口"""
|
||||
|
||||
def get(self, request):
|
||||
"""获取项目分布数据"""
|
||||
try:
|
||||
# 按项目类别统计
|
||||
category_stats = Project.objects.values('category').annotate(count=Count('id'))
|
||||
|
||||
# 格式化数据
|
||||
category_data = []
|
||||
for stat in category_stats:
|
||||
category = stat['category'] or '未分类'
|
||||
category_data.append({
|
||||
'type': self._get_category_name(category),
|
||||
'value': stat['count']
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取项目分布数据成功',
|
||||
'data': category_data
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取项目分布数据失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
def _get_category_name(self, category):
|
||||
"""获取项目类别名称"""
|
||||
category_map = {
|
||||
'frontend': '前端项目',
|
||||
'backend': '后端项目',
|
||||
'mobile': '移动端项目',
|
||||
'other': '其他项目'
|
||||
}
|
||||
return category_map.get(category, '未分类')
|
||||
337
backend/apps/views/environment.py
Normal file
337
backend/apps/views/environment.py
Normal file
@@ -0,0 +1,337 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from ..models import Environment, User
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.permissions import get_user_permissions
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class EnvironmentTypeView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取环境类型列表"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有环境查看权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
environment_permissions = function_permissions.get('environment', [])
|
||||
|
||||
if 'view' not in environment_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有环境查看权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看环境'
|
||||
}, status=403)
|
||||
|
||||
# 获取所有环境
|
||||
query = Q()
|
||||
|
||||
# 应用环境权限过滤
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if not permitted_environment_types:
|
||||
# 如果设置了自定义环境权限但列表为空,意味着没有权限查看任何环境
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何环境')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取环境列表成功',
|
||||
'data': []
|
||||
})
|
||||
|
||||
# 限制只能查看有权限的环境类型
|
||||
query &= Q(type__in=permitted_environment_types)
|
||||
|
||||
environments = Environment.objects.filter(query).order_by('name')
|
||||
|
||||
# 格式化结果
|
||||
env_list = []
|
||||
for env in environments:
|
||||
if env.type:
|
||||
env_list.append({
|
||||
'environment_id': env.environment_id,
|
||||
'type': env.type,
|
||||
'name': env.name
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取环境列表成功',
|
||||
'data': env_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取环境列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class EnvironmentView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取环境列表"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有环境查看权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
environment_permissions = function_permissions.get('environment', [])
|
||||
|
||||
if 'view' not in environment_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有环境查看权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看环境'
|
||||
}, status=403)
|
||||
|
||||
environment_id = request.GET.get('environment_id')
|
||||
name = request.GET.get('name')
|
||||
type = request.GET.get('type')
|
||||
page = int(request.GET.get('page', 1))
|
||||
page_size = int(request.GET.get('page_size', 10))
|
||||
|
||||
# 构建查询条件
|
||||
query = Q()
|
||||
if environment_id:
|
||||
query &= Q(environment_id=environment_id)
|
||||
if name:
|
||||
query &= Q(name__icontains=name)
|
||||
if type:
|
||||
query &= Q(type=type)
|
||||
|
||||
# 应用环境权限过滤
|
||||
environment_scope = data_permissions.get('environment_scope', 'all')
|
||||
if environment_scope == 'custom':
|
||||
permitted_environment_types = data_permissions.get('environment_types', [])
|
||||
if not permitted_environment_types:
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何环境')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取环境列表成功',
|
||||
'data': [],
|
||||
'total': 0,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
})
|
||||
|
||||
# 如果指定了环境类型,检查是否有权限
|
||||
if type and type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{type}]')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该类型的环境'
|
||||
}, status=403)
|
||||
|
||||
if environment_id:
|
||||
try:
|
||||
env = Environment.objects.get(environment_id=environment_id)
|
||||
if env.type not in permitted_environment_types:
|
||||
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境[{environment_id}]')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看该环境'
|
||||
}, status=403)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
}, status=404)
|
||||
|
||||
# 限制只能查看有权限的环境类型
|
||||
query &= Q(type__in=permitted_environment_types)
|
||||
|
||||
# 获取环境列表
|
||||
environments = Environment.objects.filter(query).select_related('creator')
|
||||
|
||||
# 计算总数
|
||||
total = environments.count()
|
||||
|
||||
# 分页
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
environments = environments[start:end]
|
||||
|
||||
environment_list = []
|
||||
|
||||
for env in environments:
|
||||
environment_list.append({
|
||||
'environment_id': env.environment_id,
|
||||
'name': env.name,
|
||||
'type': env.type,
|
||||
'description': env.description,
|
||||
'creator': {
|
||||
'user_id': env.creator.user_id,
|
||||
'username': env.creator.username,
|
||||
'name': env.creator.name
|
||||
} if env.creator else None,
|
||||
'create_time': env.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': env.update_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取环境列表成功',
|
||||
'data': environment_list,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取环境列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建环境"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name')
|
||||
type = data.get('type')
|
||||
description = data.get('description')
|
||||
|
||||
if not all([name, type]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '环境名称和类型不能为空'
|
||||
})
|
||||
|
||||
# 检查环境名称是否已存在
|
||||
if Environment.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '环境名称已存在'
|
||||
})
|
||||
|
||||
# 创建环境
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
environment = Environment.objects.create(
|
||||
environment_id=generate_id(),
|
||||
name=name,
|
||||
type=type,
|
||||
description=description,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建环境成功',
|
||||
'data': {
|
||||
'environment_id': environment.environment_id,
|
||||
'name': environment.name
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建环境失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""编辑环境"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
environment_id = data.get('environment_id')
|
||||
name = data.get('name')
|
||||
type = data.get('type')
|
||||
description = data.get('description')
|
||||
|
||||
if not environment_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '环境ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
environment = Environment.objects.get(environment_id=environment_id)
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
})
|
||||
|
||||
# 检查名称是否已存在(排除当前环境)
|
||||
if name and name != environment.name:
|
||||
if Environment.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '环境名称已存在'
|
||||
})
|
||||
environment.name = name
|
||||
|
||||
if type:
|
||||
environment.type = type
|
||||
if description is not None:
|
||||
environment.description = description
|
||||
|
||||
environment.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新环境成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新环境失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除环境"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
environment_id = data.get('environment_id')
|
||||
|
||||
if not environment_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '环境ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
environment = Environment.objects.get(environment_id=environment_id)
|
||||
environment.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除环境成功'
|
||||
})
|
||||
except Environment.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '环境不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除环境失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
183
backend/apps/views/gitlab.py
Normal file
183
backend/apps/views/gitlab.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import json
|
||||
import logging
|
||||
import gitlab
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from ..models import Project, BuildTask, GitlabTokenCredential
|
||||
from ..utils.auth import jwt_auth_required
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def get_gitlab_client(repository, git_token=None):
|
||||
"""获取GitLab客户端"""
|
||||
try:
|
||||
if not git_token:
|
||||
# 获取第一个可用的GitLab Token凭证
|
||||
credential = GitlabTokenCredential.objects.first()
|
||||
if not credential:
|
||||
raise ValueError('未找到GitLab Token凭证')
|
||||
git_token = credential.token
|
||||
|
||||
# 从仓库地址中提取GitLab实例URL
|
||||
repository_parts = repository.split('/')
|
||||
gitlab_url = '/'.join(repository_parts[:3]) # 获取到域名部分
|
||||
if not gitlab_url.startswith('http'):
|
||||
gitlab_url = f'http://{gitlab_url}'
|
||||
|
||||
# 创建GitLab客户端
|
||||
gl = gitlab.Gitlab(
|
||||
url=gitlab_url,
|
||||
private_token=git_token
|
||||
)
|
||||
gl.auth()
|
||||
return gl
|
||||
except Exception as e:
|
||||
logger.error(f'获取GitLab客户端失败: {str(e)}', exc_info=True)
|
||||
raise
|
||||
|
||||
def get_gitlab_project(repository, git_token=None):
|
||||
"""获取GitLab项目"""
|
||||
try:
|
||||
gl = get_gitlab_client(repository, git_token)
|
||||
repository_parts = repository.split('/')
|
||||
project_path = '/'.join(repository_parts[3:]) # 获取group/project部分
|
||||
project_path = project_path.replace('.git', '')
|
||||
return gl.projects.get(project_path)
|
||||
except Exception as e:
|
||||
logger.error(f'获取GitLab项目失败: {str(e)}', exc_info=True)
|
||||
raise
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class GitlabBranchView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取Git分支列表"""
|
||||
try:
|
||||
task_id = request.GET.get('task_id')
|
||||
if not task_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '缺少任务ID'
|
||||
})
|
||||
|
||||
# 获取任务信息
|
||||
try:
|
||||
task = BuildTask.objects.select_related('project', 'git_token').get(task_id=task_id)
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
|
||||
if not task.project or not task.project.repository:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务未配置Git仓库'
|
||||
})
|
||||
|
||||
# 获取GitLab项目
|
||||
gitlab_project = get_gitlab_project(
|
||||
task.project.repository,
|
||||
task.git_token.token if task.git_token else None
|
||||
)
|
||||
|
||||
# 获取分支列表
|
||||
branches = gitlab_project.branches.list(all=True)
|
||||
branch_list = []
|
||||
for branch in branches:
|
||||
branch_list.append({
|
||||
'name': branch.name,
|
||||
'protected': branch.protected,
|
||||
'merged': branch.merged,
|
||||
'default': branch.default,
|
||||
'commit': {
|
||||
'id': branch.commit['id'],
|
||||
'title': branch.commit['title'],
|
||||
'author_name': branch.commit['author_name'],
|
||||
'authored_date': branch.commit['authored_date'],
|
||||
}
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取分支列表成功',
|
||||
'data': branch_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取分支列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class GitlabCommitView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取Git提交记录"""
|
||||
try:
|
||||
task_id = request.GET.get('task_id')
|
||||
branch = request.GET.get('branch')
|
||||
|
||||
if not all([task_id, branch]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '缺少必要参数'
|
||||
})
|
||||
|
||||
# 获取任务信息
|
||||
try:
|
||||
task = BuildTask.objects.select_related('project', 'git_token').get(task_id=task_id)
|
||||
except BuildTask.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '任务不存在'
|
||||
})
|
||||
|
||||
if not task.project or not task.project.repository:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '任务未配置Git仓库'
|
||||
})
|
||||
|
||||
# 获取GitLab项目
|
||||
gitlab_project = get_gitlab_project(
|
||||
task.project.repository,
|
||||
task.git_token.token if task.git_token else None
|
||||
)
|
||||
|
||||
# 获取最近的提交记录
|
||||
commits = gitlab_project.commits.list(
|
||||
ref_name=branch,
|
||||
all=False,
|
||||
per_page=20, # 增加返回数量
|
||||
order_by='created_at'
|
||||
)
|
||||
|
||||
commit_list = []
|
||||
for commit in commits:
|
||||
commit_list.append({
|
||||
'id': commit.id,
|
||||
'short_id': commit.short_id,
|
||||
'title': commit.title,
|
||||
'message': commit.message,
|
||||
'author_name': commit.author_name,
|
||||
'author_email': commit.author_email,
|
||||
'authored_date': commit.authored_date,
|
||||
'created_at': commit.created_at,
|
||||
'web_url': commit.web_url
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取提交记录成功',
|
||||
'data': commit_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取提交记录失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
221
backend/apps/views/login.py
Normal file
221
backend/apps/views/login.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import json
|
||||
import hashlib
|
||||
import jwt
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.conf import settings
|
||||
from ..models import User, UserToken, LoginLog
|
||||
from ..utils.security import SecurityValidator
|
||||
|
||||
def generate_token_id():
|
||||
"""生成token_id"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
def generate_log_id():
|
||||
"""生成log_id"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def login(request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
# 获取客户端IP和用户代理
|
||||
ip_address = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', ''))
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not username or not password:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户名和密码不能为空'
|
||||
})
|
||||
|
||||
# 密码加密
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# 记录登录日志
|
||||
log_id = generate_log_id()
|
||||
|
||||
# 检查账户是否被锁定
|
||||
is_not_locked, lockout_message = SecurityValidator.check_account_lockout(user, ip_address)
|
||||
if not is_not_locked:
|
||||
# 账户被锁定,记录失败登录
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason=lockout_message
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 423, # 423 Locked
|
||||
'message': lockout_message
|
||||
})
|
||||
|
||||
if user.password != password_hash:
|
||||
# 密码错误,记录失败尝试
|
||||
failed_attempts, max_attempts = SecurityValidator.record_failed_login(user, ip_address)
|
||||
|
||||
# 记录失败登录日志
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='密码错误'
|
||||
)
|
||||
|
||||
# 提供剩余尝试次数信息
|
||||
remaining_attempts = max_attempts - failed_attempts
|
||||
if remaining_attempts > 0:
|
||||
message = f'密码错误,还可尝试{remaining_attempts}次'
|
||||
else:
|
||||
config = SecurityValidator.get_security_config()
|
||||
message = f'密码错误次数过多,账户已被锁定{config.lockout_duration}分钟'
|
||||
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': message
|
||||
})
|
||||
|
||||
if user.status == 0:
|
||||
# 账号锁定,记录失败登录
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='账号已被锁定'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 423, # 423 Locked
|
||||
'message': '账号已被锁定,请联系管理员解锁'
|
||||
})
|
||||
|
||||
# 登录成功,重置失败尝试次数
|
||||
SecurityValidator.record_successful_login(user, ip_address)
|
||||
|
||||
# 检查会话超时设置
|
||||
config = SecurityValidator.get_security_config()
|
||||
|
||||
# 生成token_id和JWT token
|
||||
token_id = generate_token_id()
|
||||
token_payload = {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=config.session_timeout) # 使用配置的会话超时时间
|
||||
}
|
||||
token = jwt.encode(token_payload, settings.SECRET_KEY, algorithm='HS256')
|
||||
|
||||
# 更新或创建token记录
|
||||
UserToken.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
'token_id': token_id,
|
||||
'token': token
|
||||
}
|
||||
)
|
||||
|
||||
# 更新登录时间
|
||||
user.login_time = datetime.now()
|
||||
user.save()
|
||||
|
||||
# 记录成功登录日志
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='success'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '登录成功',
|
||||
'data': {
|
||||
'token_id': token_id,
|
||||
'token': token,
|
||||
'user': {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'email': user.email
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except User.DoesNotExist:
|
||||
# 用户不存在,记录失败登录
|
||||
log_id = generate_log_id()
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='用户不存在'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def logout(request):
|
||||
try:
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '未提供Token'
|
||||
})
|
||||
|
||||
try:
|
||||
# 解析token获取user_id
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
user_id = payload.get('user_id')
|
||||
|
||||
# 删除该用户的token记录
|
||||
UserToken.objects.filter(user_id=user_id).delete()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '退出成功'
|
||||
})
|
||||
except jwt.ExpiredSignatureError:
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': 'Token已过期'
|
||||
})
|
||||
except jwt.InvalidTokenError:
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': '无效的Token'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
121
backend/apps/views/logs.py
Normal file
121
backend/apps/views/logs.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import json
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator
|
||||
from ..models import LoginLog, User
|
||||
from ..utils.auth import jwt_auth_required
|
||||
|
||||
@csrf_exempt
|
||||
@jwt_auth_required
|
||||
@require_http_methods(["GET"])
|
||||
def login_logs_list(request):
|
||||
"""
|
||||
获取登录日志列表
|
||||
"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
page = int(request.GET.get('page', 1))
|
||||
page_size = int(request.GET.get('page_size', 10))
|
||||
username = request.GET.get('username', '')
|
||||
status = request.GET.get('status', '')
|
||||
ip_address = request.GET.get('ip_address', '')
|
||||
start_time = request.GET.get('start_time', '')
|
||||
end_time = request.GET.get('end_time', '')
|
||||
|
||||
# 构建查询条件
|
||||
query = Q()
|
||||
|
||||
if username:
|
||||
users = User.objects.filter(username__icontains=username)
|
||||
query &= Q(user__in=users)
|
||||
|
||||
if status:
|
||||
query &= Q(status=status)
|
||||
|
||||
if ip_address:
|
||||
query &= Q(ip_address__icontains=ip_address)
|
||||
|
||||
if start_time:
|
||||
query &= Q(login_time__gte=start_time)
|
||||
|
||||
if end_time:
|
||||
query &= Q(login_time__lte=end_time)
|
||||
|
||||
# 获取登录日志
|
||||
logs = LoginLog.objects.filter(query).select_related('user').order_by('-login_time')
|
||||
|
||||
# 分页
|
||||
paginator = Paginator(logs, page_size)
|
||||
current_page = paginator.page(page)
|
||||
|
||||
# 格式化返回数据
|
||||
log_list = []
|
||||
for log in current_page.object_list:
|
||||
log_data = {
|
||||
'log_id': log.log_id,
|
||||
'username': log.user.username if log.user else None,
|
||||
'user_id': log.user.user_id if log.user else None,
|
||||
'ip_address': log.ip_address,
|
||||
'user_agent': log.user_agent,
|
||||
'status': log.status,
|
||||
'fail_reason': log.fail_reason,
|
||||
'login_time': log.login_time.strftime('%Y-%m-%d %H:%M:%S') if log.login_time else None
|
||||
}
|
||||
log_list.append(log_data)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取登录日志成功',
|
||||
'data': {
|
||||
'total': paginator.count,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'logs': log_list
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@csrf_exempt
|
||||
@jwt_auth_required
|
||||
@require_http_methods(["GET"])
|
||||
def login_log_detail(request, log_id):
|
||||
"""
|
||||
获取登录日志详情
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
log = LoginLog.objects.select_related('user').get(log_id=log_id)
|
||||
except LoginLog.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '登录日志不存在'
|
||||
})
|
||||
|
||||
log_data = {
|
||||
'log_id': log.log_id,
|
||||
'username': log.user.username if log.user else None,
|
||||
'user_id': log.user.user_id if log.user else None,
|
||||
'user_name': log.user.name if log.user else None,
|
||||
'ip_address': log.ip_address,
|
||||
'user_agent': log.user_agent,
|
||||
'status': log.status,
|
||||
'fail_reason': log.fail_reason,
|
||||
'login_time': log.login_time.strftime('%Y-%m-%d %H:%M:%S') if log.login_time else None
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取登录日志详情成功',
|
||||
'data': log_data
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
401
backend/apps/views/notification.py
Normal file
401
backend/apps/views/notification.py
Normal file
@@ -0,0 +1,401 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
from urllib.parse import quote_plus
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from ..models import NotificationRobot, User
|
||||
from ..utils.auth import jwt_auth_required
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class NotificationRobotView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request, robot_id=None):
|
||||
"""获取机器人列表或单个机器人详情"""
|
||||
try:
|
||||
if robot_id:
|
||||
try:
|
||||
robot = NotificationRobot.objects.select_related('creator').get(robot_id=robot_id)
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取机器人详情成功',
|
||||
'data': {
|
||||
'robot_id': robot.robot_id,
|
||||
'type': robot.type,
|
||||
'name': robot.name,
|
||||
'webhook': robot.webhook,
|
||||
'security_type': robot.security_type,
|
||||
'secret': robot.secret,
|
||||
'keywords': robot.keywords,
|
||||
'ip_list': robot.ip_list,
|
||||
'remark': robot.remark,
|
||||
'creator': {
|
||||
'user_id': robot.creator.user_id,
|
||||
'name': robot.creator.name
|
||||
} if robot.creator else None,
|
||||
'create_time': robot.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': robot.update_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
except NotificationRobot.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '机器人不存在'
|
||||
})
|
||||
|
||||
# 获取所有机器人列表
|
||||
robots = NotificationRobot.objects.select_related('creator').all()
|
||||
robot_list = []
|
||||
for robot in robots:
|
||||
robot_list.append({
|
||||
'robot_id': robot.robot_id,
|
||||
'type': robot.type,
|
||||
'name': robot.name,
|
||||
'webhook': robot.webhook,
|
||||
'security_type': robot.security_type,
|
||||
'secret': robot.secret,
|
||||
'keywords': robot.keywords,
|
||||
'ip_list': robot.ip_list,
|
||||
'remark': robot.remark,
|
||||
'creator': {
|
||||
'user_id': robot.creator.user_id,
|
||||
'name': robot.creator.name
|
||||
} if robot.creator else None,
|
||||
'create_time': robot.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取机器人列表成功',
|
||||
'data': robot_list
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取机器人失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建机器人"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
robot_type = data.get('type')
|
||||
name = data.get('name')
|
||||
webhook = data.get('webhook')
|
||||
security_type = data.get('security_type', 'none')
|
||||
secret = data.get('secret')
|
||||
keywords = data.get('keywords', [])
|
||||
ip_list = data.get('ip_list', [])
|
||||
remark = data.get('remark')
|
||||
|
||||
# 验证必要字段
|
||||
if not all([robot_type, name, webhook]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '机器人类型、名称和Webhook地址不能为空'
|
||||
})
|
||||
|
||||
# 验证机器人类型
|
||||
if robot_type not in ['dingtalk', 'wecom', 'feishu']:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '不支持的机器人类型'
|
||||
})
|
||||
|
||||
# 验证安全设置
|
||||
if security_type == 'secret' and not secret:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用加签密钥时,密钥不能为空'
|
||||
})
|
||||
elif security_type == 'keyword' and not keywords:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用自定义关键词时,关键词不能为空'
|
||||
})
|
||||
elif security_type == 'ip' and not ip_list:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用IP白名单时,IP列表不能为空'
|
||||
})
|
||||
|
||||
# 创建机器人
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
robot = NotificationRobot.objects.create(
|
||||
robot_id=generate_id(),
|
||||
type=robot_type,
|
||||
name=name,
|
||||
webhook=webhook,
|
||||
security_type=security_type,
|
||||
secret=secret,
|
||||
keywords=keywords,
|
||||
ip_list=ip_list,
|
||||
remark=remark,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建机器人成功',
|
||||
'data': {
|
||||
'robot_id': robot.robot_id
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'创建机器人失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""更新机器人"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
robot_id = data.get('robot_id')
|
||||
name = data.get('name')
|
||||
webhook = data.get('webhook')
|
||||
security_type = data.get('security_type')
|
||||
secret = data.get('secret')
|
||||
keywords = data.get('keywords')
|
||||
ip_list = data.get('ip_list')
|
||||
remark = data.get('remark')
|
||||
|
||||
if not robot_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '机器人ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
robot = NotificationRobot.objects.get(robot_id=robot_id)
|
||||
except NotificationRobot.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '机器人不存在'
|
||||
})
|
||||
|
||||
# 验证必要字段
|
||||
if name:
|
||||
robot.name = name
|
||||
if webhook:
|
||||
robot.webhook = webhook
|
||||
if security_type:
|
||||
robot.security_type = security_type
|
||||
if secret is not None:
|
||||
robot.secret = secret
|
||||
if keywords is not None:
|
||||
robot.keywords = keywords
|
||||
if ip_list is not None:
|
||||
robot.ip_list = ip_list
|
||||
if remark is not None:
|
||||
robot.remark = remark
|
||||
|
||||
# 验证安全设置
|
||||
if robot.security_type == 'secret' and not robot.secret:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用加签密钥时,密钥不能为空'
|
||||
})
|
||||
elif robot.security_type == 'keyword' and not robot.keywords:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用自定义关键词时,关键词不能为空'
|
||||
})
|
||||
elif robot.security_type == 'ip' and not robot.ip_list:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '使用IP白名单时,IP列表不能为空'
|
||||
})
|
||||
|
||||
robot.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新机器人成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'更新机器人失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除机器人"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
robot_id = data.get('robot_id')
|
||||
|
||||
if not robot_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '机器人ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
robot = NotificationRobot.objects.get(robot_id=robot_id)
|
||||
robot.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除机器人成功'
|
||||
})
|
||||
except NotificationRobot.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '机器人不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除机器人失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class NotificationTestView(View):
|
||||
def _sign_dingtalk(self, secret, timestamp):
|
||||
"""钉钉机器人签名"""
|
||||
string_to_sign = f'{timestamp}\n{secret}'
|
||||
hmac_code = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
string_to_sign.encode('utf-8'),
|
||||
digestmod=hashlib.sha256
|
||||
).digest()
|
||||
return base64.b64encode(hmac_code).decode('utf-8')
|
||||
|
||||
def _sign_feishu(self, secret, timestamp):
|
||||
"""飞书机器人签名"""
|
||||
string_to_sign = f'{timestamp}\n{secret}'
|
||||
hmac_code = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
string_to_sign.encode('utf-8'),
|
||||
digestmod=hashlib.sha256
|
||||
).digest()
|
||||
return base64.b64encode(hmac_code).decode('utf-8')
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""测试机器人"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
robot_id = data.get('robot_id')
|
||||
|
||||
if not robot_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '机器人ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
robot = NotificationRobot.objects.get(robot_id=robot_id)
|
||||
except NotificationRobot.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '机器人不存在'
|
||||
})
|
||||
|
||||
# 准备测试消息
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
test_message = "这是一条测试消息,如果您收到了这条消息,说明机器人配置正确。"
|
||||
|
||||
# 根据不同类型的机器人发送测试消息
|
||||
try:
|
||||
if robot.type == 'dingtalk':
|
||||
# 钉钉机器人
|
||||
webhook = robot.webhook
|
||||
|
||||
# 如果使用加签方式
|
||||
if robot.security_type == 'secret' and robot.secret:
|
||||
sign = self._sign_dingtalk(robot.secret, timestamp)
|
||||
webhook = f"{webhook}×tamp={timestamp}&sign={quote_plus(sign)}"
|
||||
|
||||
# 构建消息内容
|
||||
message_data = {
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": test_message
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(webhook, json=message_data)
|
||||
|
||||
elif robot.type == 'wecom':
|
||||
# 企业微信机器人
|
||||
response = requests.post(robot.webhook, json={
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": test_message
|
||||
}
|
||||
})
|
||||
|
||||
elif robot.type == 'feishu':
|
||||
# 飞书机器人
|
||||
headers = {}
|
||||
if robot.security_type == 'secret' and robot.secret:
|
||||
sign = self._sign_feishu(robot.secret, timestamp)
|
||||
headers.update({
|
||||
"X-Timestamp": timestamp,
|
||||
"X-Sign": sign
|
||||
})
|
||||
|
||||
response = requests.post(robot.webhook, json={
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": test_message
|
||||
}
|
||||
}, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
resp_json = response.json()
|
||||
if resp_json.get('errcode') == 0 or resp_json.get('StatusCode') == 0 or resp_json.get('code') == 0:
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '测试消息发送成功'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': f'测试消息发送失败: {response.text}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': f'测试消息发送失败: {response.text}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'发送测试消息失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'发送测试消息失败: {str(e)}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'测试机器人失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
392
backend/apps/views/project.py
Normal file
392
backend/apps/views/project.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from ..models import Project, User
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.permissions import get_user_permissions
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class ProjectView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取项目列表"""
|
||||
try:
|
||||
# 获取当前用户的权限信息
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
data_permissions = user_permissions.get('data', {})
|
||||
|
||||
# 检查用户是否有项目查看权限
|
||||
function_permissions = user_permissions.get('function', {})
|
||||
project_permissions = function_permissions.get('project', [])
|
||||
|
||||
if 'view' not in project_permissions:
|
||||
logger.warning(f'用户[{request.user_id}]没有项目查看权限')
|
||||
return JsonResponse({
|
||||
'code': 403,
|
||||
'message': '没有权限查看项目'
|
||||
}, status=403)
|
||||
|
||||
project_id = request.GET.get('project_id')
|
||||
name = request.GET.get('name')
|
||||
category = request.GET.get('category')
|
||||
|
||||
# 构建查询条件
|
||||
query = Q()
|
||||
|
||||
if project_id:
|
||||
query &= Q(project_id=project_id)
|
||||
if name:
|
||||
query &= Q(name__icontains=name) # 使用 icontains 进行不区分大小写的模糊查询
|
||||
if category:
|
||||
query &= Q(category=category)
|
||||
|
||||
# 应用项目权限过滤
|
||||
project_scope = data_permissions.get('project_scope', 'all')
|
||||
if project_scope == 'custom':
|
||||
permitted_project_ids = data_permissions.get('project_ids', [])
|
||||
if not permitted_project_ids:
|
||||
# 如果设置了自定义项目权限但列表为空,意味着没有权限查看任何项目
|
||||
logger.info(f'用户[{request.user_id}]没有权限查看任何项目')
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取项目列表成功',
|
||||
'data': []
|
||||
})
|
||||
|
||||
# 限制只能查看有权限的项目
|
||||
query &= Q(project_id__in=permitted_project_ids)
|
||||
|
||||
# 使用查询条件过滤项目
|
||||
projects = Project.objects.select_related('creator').filter(query)
|
||||
|
||||
project_list = []
|
||||
for project in projects:
|
||||
project_list.append({
|
||||
'project_id': project.project_id,
|
||||
'name': project.name,
|
||||
'description': project.description,
|
||||
'category': project.category,
|
||||
'repository': project.repository,
|
||||
'creator': {
|
||||
'user_id': project.creator.user_id,
|
||||
'username': project.creator.username,
|
||||
'name': project.creator.name
|
||||
},
|
||||
'create_time': project.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': project.update_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取项目列表成功',
|
||||
'data': project_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取项目列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建项目"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
category = data.get('category')
|
||||
repository = data.get('repository')
|
||||
|
||||
if not name:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目名称不能为空'
|
||||
})
|
||||
|
||||
# 检查项目名称是否已存在
|
||||
if Project.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目名称已存在'
|
||||
})
|
||||
|
||||
# 创建项目
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
project = Project.objects.create(
|
||||
project_id=generate_id(),
|
||||
name=name,
|
||||
description=description,
|
||||
category=category,
|
||||
repository=repository,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建项目成功',
|
||||
'data': {
|
||||
'project_id': project.project_id,
|
||||
'name': project.name
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建项目失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""编辑项目"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
project_id = data.get('project_id')
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
category = data.get('category')
|
||||
repository = data.get('repository')
|
||||
|
||||
if not project_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
project = Project.objects.get(project_id=project_id)
|
||||
except Project.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '项目不存在'
|
||||
})
|
||||
|
||||
# 检查项目名称是否已存在
|
||||
if name and name != project.name:
|
||||
if Project.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目名称已存在'
|
||||
})
|
||||
project.name = name
|
||||
|
||||
if description is not None:
|
||||
project.description = description
|
||||
if category:
|
||||
project.category = category
|
||||
if repository:
|
||||
project.repository = repository
|
||||
|
||||
project.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新项目成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新项目失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除项目"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
project_id = data.get('project_id')
|
||||
|
||||
if not project_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
project = Project.objects.get(project_id=project_id)
|
||||
project.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除项目成功'
|
||||
})
|
||||
except Project.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '项目不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除项目失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class ProjectServiceView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建项目服务"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
project_id = data.get('project_id')
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
category = data.get('category')
|
||||
repository = data.get('repository')
|
||||
|
||||
if not all([project_id, name, category, repository]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '项目ID、服务名称、类别和仓库地址不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
project = Project.objects.get(project_id=project_id)
|
||||
except Project.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '项目不存在'
|
||||
})
|
||||
|
||||
# 检查服务名称在项目下是否已存在
|
||||
if ProjectService.objects.filter(project=project, name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '该项目下已存在同名服务'
|
||||
})
|
||||
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
service = ProjectService.objects.create(
|
||||
service_id=generate_id(),
|
||||
project=project,
|
||||
name=name,
|
||||
description=description,
|
||||
category=category,
|
||||
repository=repository,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建服务成功',
|
||||
'data': {
|
||||
'service_id': service.service_id,
|
||||
'name': service.name
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建服务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""更新项目服务"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
service_id = data.get('service_id')
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
category = data.get('category')
|
||||
repository = data.get('repository')
|
||||
|
||||
if not service_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '服务ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
service = ProjectService.objects.get(service_id=service_id)
|
||||
except ProjectService.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '服务不存在'
|
||||
})
|
||||
|
||||
# 检查服务名称是否已存在
|
||||
if name and name != service.name:
|
||||
if ProjectService.objects.filter(project=service.project, name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '该项目下已存在同名服务'
|
||||
})
|
||||
service.name = name
|
||||
|
||||
if description is not None:
|
||||
service.description = description
|
||||
if category:
|
||||
service.category = category
|
||||
if repository:
|
||||
service.repository = repository
|
||||
|
||||
service.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新服务成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新服务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除项目服务"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
service_id = data.get('service_id')
|
||||
|
||||
if not service_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '服务ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
service = ProjectService.objects.get(service_id=service_id)
|
||||
service.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除服务成功'
|
||||
})
|
||||
except ProjectService.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '服务不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除服务失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
346
backend/apps/views/role.py
Normal file
346
backend/apps/views/role.py
Normal file
@@ -0,0 +1,346 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from ..models import User, Role, UserRole
|
||||
from ..utils.auth import jwt_auth_required
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class RoleView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取角色列表"""
|
||||
try:
|
||||
role_id = request.GET.get('role_id')
|
||||
name = request.GET.get('name')
|
||||
|
||||
# 构建查询条件
|
||||
query = {}
|
||||
|
||||
if role_id:
|
||||
query['role_id'] = role_id
|
||||
if name:
|
||||
query['name__icontains'] = name
|
||||
|
||||
# 使用查询条件过滤角色
|
||||
roles = Role.objects.select_related('creator').filter(**query)
|
||||
|
||||
role_list = []
|
||||
for role in roles:
|
||||
# 处理permissions字段,确保是对象形式
|
||||
permissions = role.permissions
|
||||
if isinstance(permissions, str):
|
||||
try:
|
||||
permissions = json.loads(permissions)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f'角色[{role.name}]的权限数据格式错误')
|
||||
permissions = {}
|
||||
|
||||
role_data = {
|
||||
'role_id': role.role_id,
|
||||
'name': role.name,
|
||||
'description': role.description,
|
||||
'permissions': permissions, # 返回处理后的权限配置
|
||||
'creator': {
|
||||
'user_id': role.creator.user_id,
|
||||
'username': role.creator.username,
|
||||
'name': role.creator.name
|
||||
} if role.creator else None,
|
||||
'create_time': role.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'update_time': role.update_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
}
|
||||
role_list.append(role_data)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取角色列表成功',
|
||||
'data': role_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取角色列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建角色"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
permissions = data.get('permissions', {})
|
||||
|
||||
if not name:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '角色名称不能为空'
|
||||
})
|
||||
|
||||
# 检查角色名称是否已存在
|
||||
if Role.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '角色名称已存在'
|
||||
})
|
||||
|
||||
# 创建角色
|
||||
creator = User.objects.get(user_id=request.user_id)
|
||||
role = Role.objects.create(
|
||||
role_id=generate_id(),
|
||||
name=name,
|
||||
description=description,
|
||||
permissions=permissions,
|
||||
creator=creator
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建角色成功',
|
||||
'data': {
|
||||
'role_id': role.role_id,
|
||||
'name': role.name
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建角色失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""编辑角色"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
role_id = data.get('role_id')
|
||||
name = data.get('name')
|
||||
description = data.get('description')
|
||||
permissions = data.get('permissions')
|
||||
|
||||
if not role_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '角色ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
role = Role.objects.get(role_id=role_id)
|
||||
except Role.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '角色不存在'
|
||||
})
|
||||
|
||||
# 检查角色名称是否已存在
|
||||
if name and name != role.name:
|
||||
if Role.objects.filter(name=name).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '角色名称已存在'
|
||||
})
|
||||
role.name = name
|
||||
|
||||
if description is not None:
|
||||
role.description = description
|
||||
|
||||
# 处理permissions字段
|
||||
if permissions is not None:
|
||||
# 确保permissions是对象而不是字符串
|
||||
if isinstance(permissions, str):
|
||||
try:
|
||||
permissions = json.loads(permissions)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '权限数据格式错误'
|
||||
})
|
||||
role.permissions = permissions
|
||||
|
||||
role.save()
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新角色成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新角色失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除角色"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
role_id = data.get('role_id')
|
||||
|
||||
if not role_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '角色ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
role = Role.objects.get(role_id=role_id)
|
||||
|
||||
# 检查是否有用户使用该角色
|
||||
if UserRole.objects.filter(role=role).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '该角色已分配给用户,无法删除'
|
||||
})
|
||||
|
||||
role.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除角色成功'
|
||||
})
|
||||
except Role.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '角色不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除角色失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserPermissionView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取当前用户的权限"""
|
||||
try:
|
||||
user_id = request.user_id
|
||||
try:
|
||||
user = User.objects.get(user_id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
# 记录操作日志
|
||||
logger.info(f'用户[{user.username}]获取权限信息')
|
||||
|
||||
# 获取用户的所有角色
|
||||
user_roles = UserRole.objects.filter(user=user).select_related('role')
|
||||
|
||||
# 合并所有角色的权限
|
||||
menu_permissions = set()
|
||||
function_permissions = {}
|
||||
data_permissions = {
|
||||
'project_scope': 'all',
|
||||
'project_ids': [],
|
||||
'environment_scope': 'all',
|
||||
'environment_types': []
|
||||
}
|
||||
|
||||
has_custom_project_scope = False
|
||||
has_custom_environment_scope = False
|
||||
|
||||
for user_role in user_roles:
|
||||
role = user_role.role
|
||||
permissions = role.permissions
|
||||
|
||||
# 如果permissions是字符串,解析为JSON
|
||||
if isinstance(permissions, str):
|
||||
try:
|
||||
permissions = json.loads(permissions)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f'解析角色[{role.name}]的权限数据失败')
|
||||
permissions = {}
|
||||
|
||||
# 合并菜单权限
|
||||
if permissions.get('menu') and isinstance(permissions['menu'], list):
|
||||
menu_permissions.update(permissions['menu'])
|
||||
|
||||
# 合并功能权限
|
||||
if permissions.get('function') and isinstance(permissions['function'], dict):
|
||||
for module, actions in permissions['function'].items():
|
||||
if not isinstance(actions, list):
|
||||
continue
|
||||
|
||||
if module not in function_permissions:
|
||||
function_permissions[module] = []
|
||||
function_permissions[module].extend(actions)
|
||||
# 确保不重复
|
||||
function_permissions[module] = list(set(function_permissions[module]))
|
||||
|
||||
# 合并数据权限
|
||||
if permissions.get('data') and isinstance(permissions['data'], dict):
|
||||
data_perms = permissions['data']
|
||||
|
||||
# 项目权限
|
||||
if data_perms.get('project_scope') == 'custom':
|
||||
has_custom_project_scope = True
|
||||
if data_permissions['project_scope'] == 'all':
|
||||
data_permissions['project_scope'] = 'custom'
|
||||
data_permissions['project_ids'] = data_perms.get('project_ids', [])
|
||||
else:
|
||||
# 合并项目ID列表
|
||||
data_permissions['project_ids'].extend(data_perms.get('project_ids', []))
|
||||
# 确保不重复
|
||||
data_permissions['project_ids'] = list(set(data_permissions['project_ids']))
|
||||
|
||||
# 环境权限
|
||||
if data_perms.get('environment_scope') == 'custom':
|
||||
has_custom_environment_scope = True
|
||||
if data_permissions['environment_scope'] == 'all':
|
||||
data_permissions['environment_scope'] = 'custom'
|
||||
data_permissions['environment_types'] = data_perms.get('environment_types', [])
|
||||
else:
|
||||
# 合并环境类型列表
|
||||
data_permissions['environment_types'].extend(data_perms.get('environment_types', []))
|
||||
# 确保不重复
|
||||
data_permissions['environment_types'] = list(set(data_permissions['environment_types']))
|
||||
|
||||
# 如果没有任何角色有自定义项目/环境范围,保持为'all'
|
||||
if not has_custom_project_scope:
|
||||
data_permissions['project_scope'] = 'all'
|
||||
data_permissions['project_ids'] = []
|
||||
|
||||
if not has_custom_environment_scope:
|
||||
data_permissions['environment_scope'] = 'all'
|
||||
data_permissions['environment_types'] = []
|
||||
|
||||
permissions_result = {
|
||||
'menu': list(menu_permissions),
|
||||
'function': function_permissions,
|
||||
'data': data_permissions
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取用户权限成功',
|
||||
'data': permissions_result
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取用户权限失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
143
backend/apps/views/security.py
Normal file
143
backend/apps/views/security.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from ..models import SecurityConfig, User
|
||||
from ..utils.auth import jwt_auth_required
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class SecurityConfigView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取安全配置"""
|
||||
try:
|
||||
# 获取或创建安全配置
|
||||
security_config, created = SecurityConfig.objects.get_or_create(
|
||||
id=1,
|
||||
defaults={
|
||||
'min_password_length': 8,
|
||||
'password_complexity': ['lowercase', 'number'],
|
||||
'session_timeout': 120,
|
||||
'max_login_attempts': 5,
|
||||
'lockout_duration': 30,
|
||||
'enable_2fa': False
|
||||
}
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取安全配置成功',
|
||||
'data': {
|
||||
'min_password_length': security_config.min_password_length,
|
||||
'password_complexity': security_config.password_complexity,
|
||||
'session_timeout': security_config.session_timeout,
|
||||
'max_login_attempts': security_config.max_login_attempts,
|
||||
'lockout_duration': security_config.lockout_duration,
|
||||
'enable_2fa': security_config.enable_2fa,
|
||||
'update_time': security_config.update_time.strftime('%Y-%m-%d %H:%M:%S') if security_config.update_time else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取安全配置失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""更新安全配置"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
|
||||
min_password_length = data.get('min_password_length')
|
||||
password_complexity = data.get('password_complexity')
|
||||
session_timeout = data.get('session_timeout')
|
||||
max_login_attempts = data.get('max_login_attempts')
|
||||
lockout_duration = data.get('lockout_duration')
|
||||
enable_2fa = data.get('enable_2fa')
|
||||
|
||||
# 验证输入数据
|
||||
if min_password_length is not None:
|
||||
if not isinstance(min_password_length, int) or min_password_length < 6 or min_password_length > 20:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '密码最小长度必须在6-20之间'
|
||||
})
|
||||
|
||||
if password_complexity is not None:
|
||||
if not isinstance(password_complexity, list):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '密码复杂度要求格式错误'
|
||||
})
|
||||
valid_complexity = ['uppercase', 'lowercase', 'number', 'special']
|
||||
for item in password_complexity:
|
||||
if item not in valid_complexity:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': f'无效的密码复杂度要求: {item}'
|
||||
})
|
||||
|
||||
if session_timeout is not None:
|
||||
if not isinstance(session_timeout, int) or session_timeout < 10 or session_timeout > 1440:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '会话超时时间必须在10-1440分钟之间'
|
||||
})
|
||||
|
||||
if max_login_attempts is not None:
|
||||
if not isinstance(max_login_attempts, int) or max_login_attempts < 3 or max_login_attempts > 10:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '最大登录尝试次数必须在3-10次之间'
|
||||
})
|
||||
|
||||
if lockout_duration is not None:
|
||||
if not isinstance(lockout_duration, int) or lockout_duration < 5 or lockout_duration > 60:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '账户锁定时间必须在5-60分钟之间'
|
||||
})
|
||||
|
||||
# 获取或创建安全配置
|
||||
security_config, created = SecurityConfig.objects.get_or_create(id=1)
|
||||
|
||||
# 更新配置
|
||||
if min_password_length is not None:
|
||||
security_config.min_password_length = min_password_length
|
||||
if password_complexity is not None:
|
||||
security_config.password_complexity = password_complexity
|
||||
if session_timeout is not None:
|
||||
security_config.session_timeout = session_timeout
|
||||
if max_login_attempts is not None:
|
||||
security_config.max_login_attempts = max_login_attempts
|
||||
if lockout_duration is not None:
|
||||
security_config.lockout_duration = lockout_duration
|
||||
if enable_2fa is not None:
|
||||
security_config.enable_2fa = enable_2fa
|
||||
|
||||
security_config.save()
|
||||
|
||||
# 记录操作日志
|
||||
user = User.objects.get(user_id=request.user_id)
|
||||
logger.info(f'用户[{user.username}]更新了安全配置')
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '安全配置更新成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'更新安全配置失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
334
backend/apps/views/user.py
Normal file
334
backend/apps/views/user.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from ..models import User, Role, UserRole
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.security import SecurityValidator
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取用户列表"""
|
||||
try:
|
||||
user_id = request.GET.get('user_id')
|
||||
username = request.GET.get('username')
|
||||
email = request.GET.get('email')
|
||||
status = request.GET.get('status')
|
||||
|
||||
# 构建查询条件
|
||||
query = {}
|
||||
|
||||
if user_id:
|
||||
query['user_id'] = user_id
|
||||
if username:
|
||||
query['username__icontains'] = username # 使用 icontains 进行不区分大小写的模糊查询
|
||||
if email:
|
||||
query['email__icontains'] = email
|
||||
if status:
|
||||
query['status'] = status
|
||||
|
||||
# 使用查询条件过滤用户
|
||||
users = User.objects.filter(**query)
|
||||
|
||||
user_list = []
|
||||
for user in users:
|
||||
# 获取用户角色
|
||||
user_roles = UserRole.objects.filter(user=user).select_related('role')
|
||||
roles = [{"role_id": ur.role.role_id, "name": ur.role.name} for ur in user_roles]
|
||||
|
||||
user_list.append({
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'email': user.email,
|
||||
'status': user.status,
|
||||
'roles': roles,
|
||||
'login_time': user.login_time.strftime('%Y-%m-%d %H:%M:%S') if user.login_time else None,
|
||||
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取用户列表成功',
|
||||
'data': user_list
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取用户列表失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""创建用户"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
username = data.get('username')
|
||||
name = data.get('name')
|
||||
password = data.get('password')
|
||||
email = data.get('email')
|
||||
role_ids = data.get('role_ids', [])
|
||||
|
||||
if not all([username, name, password, email]):
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户名、姓名、密码和邮箱不能为空'
|
||||
})
|
||||
|
||||
# 检查用户名是否已存在
|
||||
if User.objects.filter(username=username).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户名已存在'
|
||||
})
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
if User.objects.filter(email=email).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '邮箱已存在'
|
||||
})
|
||||
|
||||
# 验证密码强度
|
||||
is_valid, message = SecurityValidator.validate_password(password)
|
||||
if not is_valid:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': message
|
||||
})
|
||||
|
||||
# 密码加密
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# 创建用户
|
||||
user = User.objects.create(
|
||||
user_id=generate_id(),
|
||||
username=username,
|
||||
name=name,
|
||||
password=password_hash,
|
||||
email=email,
|
||||
status=1 # 默认启用
|
||||
)
|
||||
|
||||
# 分配角色
|
||||
for role_id in role_ids:
|
||||
try:
|
||||
role = Role.objects.get(role_id=role_id)
|
||||
UserRole.objects.create(user=user, role=role)
|
||||
except Role.DoesNotExist:
|
||||
logger.warning(f'角色不存在: {role_id}')
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '创建用户成功',
|
||||
'data': {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'创建用户失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""编辑用户"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
user_id = data.get('user_id')
|
||||
name = data.get('name')
|
||||
email = data.get('email')
|
||||
status = data.get('status')
|
||||
password = data.get('password')
|
||||
role_ids = data.get('role_ids')
|
||||
|
||||
if not user_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
user = User.objects.get(user_id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
# 更新用户信息
|
||||
if name:
|
||||
user.name = name
|
||||
if email and email != user.email:
|
||||
# 检查邮箱是否已存在
|
||||
if User.objects.filter(email=email).exclude(user_id=user_id).exists():
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '邮箱已存在'
|
||||
})
|
||||
user.email = email
|
||||
if status is not None:
|
||||
# 如果是解锁操作(从0改为1),使用安全验证器的解锁方法
|
||||
if user.status == 0 and status == 1:
|
||||
success, message = SecurityValidator.unlock_user_account(user)
|
||||
if not success:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': message
|
||||
})
|
||||
# 如果是锁定操作(从1改为0),使用安全验证器的锁定方法
|
||||
elif user.status == 1 and status == 0:
|
||||
success, message = SecurityValidator.lock_user_account(user)
|
||||
if not success:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': message
|
||||
})
|
||||
else:
|
||||
user.status = status
|
||||
if password:
|
||||
# 验证密码强度
|
||||
is_valid, message = SecurityValidator.validate_password(password)
|
||||
if not is_valid:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': message
|
||||
})
|
||||
|
||||
# 密码加密
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
user.password = password_hash
|
||||
|
||||
user.save()
|
||||
|
||||
# 更新角色
|
||||
if role_ids is not None:
|
||||
# 删除旧角色关联
|
||||
UserRole.objects.filter(user=user).delete()
|
||||
|
||||
# 添加新角色关联
|
||||
for role_id in role_ids:
|
||||
try:
|
||||
role = Role.objects.get(role_id=role_id)
|
||||
UserRole.objects.create(user=user, role=role)
|
||||
except Role.DoesNotExist:
|
||||
logger.warning(f'角色不存在: {role_id}')
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '更新用户成功'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'更新用户失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def delete(self, request):
|
||||
"""删除用户"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
user_id = data.get('user_id')
|
||||
|
||||
if not user_id:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户ID不能为空'
|
||||
})
|
||||
|
||||
try:
|
||||
user = User.objects.get(user_id=user_id)
|
||||
# 删除关联的角色
|
||||
UserRole.objects.filter(user=user).delete()
|
||||
# 删除用户
|
||||
user.delete()
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '删除用户成功'
|
||||
})
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'删除用户失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserProfileView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取当前登录用户的个人信息"""
|
||||
try:
|
||||
user_id = request.user_id
|
||||
|
||||
try:
|
||||
user = User.objects.get(user_id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
# 获取用户角色
|
||||
user_roles = UserRole.objects.filter(user=user).select_related('role')
|
||||
roles = [{
|
||||
"role_id": ur.role.role_id,
|
||||
"name": ur.role.name,
|
||||
"description": ur.role.description
|
||||
} for ur in user_roles]
|
||||
|
||||
# 构建用户信息
|
||||
user_info = {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'email': user.email,
|
||||
'status': user.status,
|
||||
'roles': roles,
|
||||
'login_time': user.login_time.strftime('%Y-%m-%d %H:%M:%S') if user.login_time else None,
|
||||
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if user.create_time else None,
|
||||
'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if user.update_time else None,
|
||||
}
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取用户信息成功',
|
||||
'data': user_info
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取用户信息失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
Reference in New Issue
Block a user