first commit

This commit is contained in:
hukdoesn
2025-06-12 16:48:37 +08:00
commit 1bb6f0b9a8
44 changed files with 8808 additions and 0 deletions

924
backend/apps/views/build.py Normal file
View 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)}'
})

View 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)}'
})

View 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

View File

File diff suppressed because it is too large Load Diff

View 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, '未分类')

View 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)}'
})

View 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
View 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
View 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)}'
})

View 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}&timestamp={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)}'
})

View 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
View 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)}'
})

View 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
View 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)}'
})