feat: 清理构建日志及登陆功能

This commit is contained in:
hukdoesn
2025-07-21 14:00:32 +08:00
parent e8bf8123b0
commit 9d78b1fe7a
3 changed files with 445 additions and 9 deletions

View File

@@ -4,9 +4,13 @@ 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.views.decorators.http import require_http_methods
from django.db import transaction
from ..models import SecurityConfig, User
from django.utils import timezone
from datetime import datetime, timedelta
from ..models import SecurityConfig, User, BuildTask, BuildHistory, LoginLog
from ..utils.auth import jwt_auth_required
from ..utils.permissions import get_user_permissions
logger = logging.getLogger('apps')
@@ -140,4 +144,227 @@ class SecurityConfigView(View):
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
})
@csrf_exempt
@jwt_auth_required
@require_http_methods(["GET"])
def get_build_tasks_for_cleanup(request):
"""获取可用于日志清理的构建任务列表"""
try:
# 获取用户权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有系统基本设置权限
function_permissions = user_permissions.get('function', {})
system_permissions = function_permissions.get('system_basic', [])
if 'view' not in system_permissions:
logger.warning(f'用户[{request.user_id}]没有系统基本设置查看权限')
return JsonResponse({
'code': 403,
'message': '没有权限查看构建任务'
}, status=403)
# 应用项目权限过滤
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:
return JsonResponse({
'code': 200,
'message': '获取构建任务列表成功',
'data': []
})
tasks = BuildTask.objects.filter(project__project_id__in=permitted_project_ids).select_related('project')
else:
tasks = BuildTask.objects.all().select_related('project')
# 格式化返回数据
task_list = []
for task in tasks:
task_list.append({
'task_id': task.task_id,
'name': task.name,
'project_name': task.project.name if task.project else '未知项目',
'total_builds': task.total_builds
})
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)}'
})
@csrf_exempt
@jwt_auth_required
@require_http_methods(["POST"])
def cleanup_build_logs(request):
"""清理构建日志"""
try:
# 获取用户权限信息
user_permissions = get_user_permissions(request.user_id)
# 检查用户是否有系统基本设置编辑权限
function_permissions = user_permissions.get('function', {})
system_permissions = function_permissions.get('system_basic', [])
if 'edit' not in system_permissions:
logger.warning(f'用户[{request.user_id}]没有系统基本设置编辑权限')
return JsonResponse({
'code': 403,
'message': '没有权限执行日志清理操作'
}, status=403)
data = json.loads(request.body)
task_ids = data.get('task_ids', [])
days_before = data.get('days_before', 30)
if not isinstance(days_before, int) or days_before < 1 or days_before > 365:
return JsonResponse({
'code': 400,
'message': '保留天数必须在1-365天之间'
})
# 计算截止日期
cutoff_date = timezone.now() - timedelta(days=days_before)
# 查询要删除的构建历史记录
if task_ids:
# 清理指定任务的日志
histories_to_delete = BuildHistory.objects.filter(
task__task_id__in=task_ids,
create_time__lt=cutoff_date
)
else:
# 清理所有任务的日志
histories_to_delete = BuildHistory.objects.filter(
create_time__lt=cutoff_date
)
# 统计信息
total_count = histories_to_delete.count()
if total_count == 0:
task_desc = f"{len(task_ids)}个指定任务" if task_ids else "所有任务"
return JsonResponse({
'code': 200,
'message': f'没有找到{task_desc}中需要清理的构建日志记录',
'data': {
'deleted_count': 0,
'task_count': len(task_ids) if task_ids else 0,
'days_before': days_before
}
})
# 执行删除操作
with transaction.atomic():
deleted_count, _ = histories_to_delete.delete()
# 记录操作日志
user = User.objects.get(user_id=request.user_id)
task_desc = f"{len(task_ids)}个指定任务" if task_ids else "所有任务"
logger.info(f'用户[{user.username}]清理了{task_desc}{days_before}天前的构建日志,共删除{deleted_count}条记录')
return JsonResponse({
'code': 200,
'message': f'构建日志清理完成,共删除{deleted_count}条记录',
'data': {
'deleted_count': deleted_count,
'task_count': len(task_ids) if task_ids else 0,
'days_before': days_before
}
})
except Exception as e:
logger.error(f'清理构建日志失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@csrf_exempt
@jwt_auth_required
@require_http_methods(["POST"])
def cleanup_login_logs(request):
"""清理登录日志"""
try:
# 获取用户权限信息
user_permissions = get_user_permissions(request.user_id)
# 检查用户是否有系统基本设置编辑权限
function_permissions = user_permissions.get('function', {})
system_permissions = function_permissions.get('system_basic', [])
if 'edit' not in system_permissions:
logger.warning(f'用户[{request.user_id}]没有系统基本设置编辑权限')
return JsonResponse({
'code': 403,
'message': '没有权限执行日志清理操作'
}, status=403)
data = json.loads(request.body)
days_before = data.get('days_before', 30)
# 验证输入参数
if not isinstance(days_before, int) or days_before < 1 or days_before > 365:
return JsonResponse({
'code': 400,
'message': '保留天数必须在1-365天之间'
})
# 计算截止日期
cutoff_date = timezone.now() - timedelta(days=days_before)
# 查询要删除的登录日志记录
logs_to_delete = LoginLog.objects.filter(
login_time__lt=cutoff_date
)
# 统计信息
total_count = logs_to_delete.count()
if total_count == 0:
return JsonResponse({
'code': 200,
'message': '没有找到需要清理的登录日志记录',
'data': {
'deleted_count': 0,
'days_before': days_before
}
})
# 执行删除操作
with transaction.atomic():
deleted_count, _ = logs_to_delete.delete()
# 记录操作日志
user = User.objects.get(user_id=request.user_id)
logger.info(f'用户[{user.username}]清理了{days_before}天前的登录日志,共删除{deleted_count}条记录')
return JsonResponse({
'code': 200,
'message': f'登录日志清理完成,共删除{deleted_count}条记录',
'data': {
'deleted_count': deleted_count,
'days_before': days_before
}
})
except Exception as e:
logger.error(f'清理登录日志失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})

View File

@@ -15,7 +15,7 @@ from apps.views.logs import login_logs_list, login_log_detail
from apps.views.dashboard import DashboardStatsView, BuildTrendView, BuildDetailView, RecentBuildsView, ProjectDistributionView
from apps.views.webhook import GitLabWebhookView
from apps.views.security import SecurityConfigView
from apps.views.security import SecurityConfigView, get_build_tasks_for_cleanup, cleanup_build_logs, cleanup_login_logs
urlpatterns = [
path('admin/', admin.site.urls),
@@ -68,4 +68,7 @@ urlpatterns = [
# 安全配置相关路由
path('api/system/security/', SecurityConfigView.as_view(), name='security-config'),
path('api/system/security/build-tasks/', get_build_tasks_for_cleanup, name='build-tasks-for-cleanup'),
path('api/system/security/cleanup-build-logs/', cleanup_build_logs, name='cleanup-build-logs'),
path('api/system/security/cleanup-login-logs/', cleanup_login_logs, name='cleanup-login-logs'),
]

View File

@@ -6,7 +6,7 @@
<a-tabs v-model:activeKey="activeTabKey" @change="handleTabChange">
<!-- 安全设置 -->
<a-tab-pane key="security-config" tab="安全设置">
<a-tab-pane key="security-config" tab="设置">
<a-card>
<a-form
ref="securityFormRef"
@@ -83,9 +83,9 @@
</a-row>
<a-form-item>
<a-button
type="primary"
@click="saveSecurityConfig"
<a-button
type="primary"
@click="saveSecurityConfig"
:loading="securityLoading"
v-if="hasFunctionPermission('system_basic', 'edit')"
>
@@ -93,11 +93,99 @@
</a-button>
</a-form-item>
</a-form>
<!-- 分割线 -->
<a-divider>日志清理</a-divider>
<!-- 日志清理功能 -->
<a-form layout="vertical" v-if="hasFunctionPermission('system_basic', 'edit')">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="日志类型">
<a-select
v-model:value="logCleanupForm.logType"
placeholder="请选择日志类型"
style="width: 100%"
@change="handleLogTypeChange"
>
<a-select-option value="build">构建日志</a-select-option>
<a-select-option value="login">登录日志</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="保留天数">
<a-input-number
v-model:value="logCleanupForm.daysBefore"
:min="1"
:max="365"
style="width: 100%"
placeholder="请输入保留天数"
addon-after=""
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="操作">
<a-button
type="primary"
danger
@click="handleLogCleanup"
:loading="logCleanupLoading"
:disabled="!logCleanupForm.logType || !logCleanupForm.daysBefore"
style="width: 100%"
>
清理日志
</a-button>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24" v-if="logCleanupForm.logType === 'build'">
<a-col :span="24">
<a-form-item label="选择构建任务">
<a-select
v-model:value="logCleanupForm.selectedTasks"
mode="multiple"
placeholder="请选择要清理日志的构建任务,不选择则清理所有任务"
style="width: 100%"
:loading="buildTasksLoading"
show-search
:filter-option="filterBuildTasks"
allow-clear
>
<a-select-option
v-for="task in buildTasksList"
:key="task.task_id"
:value="task.task_id"
>
{{ task.name }} ({{ task.project_name }})
</a-select-option>
</a-select>
<div class="form-item-help">
将删除{{ logCleanupForm.selectedTasks.length > 0 ? '选定任务' : '所有任务' }}中超过{{ logCleanupForm.daysBefore || 0 }}天的构建日志
</div>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24" v-if="logCleanupForm.logType === 'login'">
<a-col :span="24">
<a-alert
message="登录日志清理说明"
:description="`将删除超过${logCleanupForm.daysBefore || 0}天的所有登录日志记录,包括成功和失败的登录记录。`"
type="info"
show-icon
style="margin-bottom: 16px"
/>
</a-col>
</a-row>
</a-form>
</a-card>
</a-tab-pane>
<!-- 通知配置 -->
<a-tab-pane key="notification-config" tab="通知配置">
<a-tab-pane key="notification-config" tab="通知">
<a-card>
<div class="notification-header">
<a-button type="primary" @click="showAddRobot" v-if="hasFunctionPermission('system_basic', 'create')">
@@ -288,7 +376,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import axios from 'axios';
import { hasFunctionPermission, checkPermission } from '../../utils/permission';
@@ -315,6 +403,16 @@ const securityRules = {
]
};
// 日志清理相关
const buildTasksList = ref([]);
const buildTasksLoading = ref(false);
const logCleanupLoading = ref(false);
const logCleanupForm = reactive({
logType: 'build',
selectedTasks: [],
daysBefore: 30
});
// 通知机器人相关
const drawerVisible = ref(false);
const submitLoading = ref(false);
@@ -388,6 +486,8 @@ const securityLoading = ref(false);
const handleTabChange = (key) => {
if (key === 'notification-config') {
loadRobotList();
} else if (key === 'security-config') {
loadBuildTasksList();
}
};
@@ -605,8 +705,114 @@ const handleDeleteRobot = async (robot) => {
}
};
// 获取构建任务列表
const loadBuildTasksList = async () => {
try {
buildTasksLoading.value = true;
const token = localStorage.getItem('token');
const response = await axios.get('/api/system/security/build-tasks/', {
headers: { 'Authorization': token }
});
if (response.data.code === 200) {
buildTasksList.value = response.data.data;
} else {
message.error(response.data.message || '获取构建任务列表失败');
}
} catch (error) {
console.error('Load build tasks error:', error);
message.error('获取构建任务列表失败');
} finally {
buildTasksLoading.value = false;
}
};
// 构建任务过滤函数
const filterBuildTasks = (input, option) => {
return option.children[0].children.toLowerCase().includes(input.toLowerCase());
};
// 日志类型变化处理
const handleLogTypeChange = () => {
// 切换日志类型清空构建任务选择
logCleanupForm.selectedTasks = [];
};
// 处理日志清理
const handleLogCleanup = async () => {
try {
// 生成确认信息
let confirmContent = '';
if (logCleanupForm.logType === 'build') {
const taskCount = logCleanupForm.selectedTasks.length;
if (taskCount > 0) {
confirmContent = `确定要清理选定的${taskCount}个构建任务中${logCleanupForm.daysBefore}天前的构建日志吗?`;
} else {
confirmContent = `确定要清理所有构建任务中${logCleanupForm.daysBefore}天前的构建日志吗?`;
}
} else if (logCleanupForm.logType === 'login') {
confirmContent = `确定要清理${logCleanupForm.daysBefore}天前的所有登录日志吗?`;
}
confirmContent += '此操作不可恢复。';
// 确认对话框
const confirmed = await new Promise((resolve) => {
Modal.confirm({
title: '确认清理日志',
content: confirmContent,
okText: '确定清理',
okType: 'danger',
cancelText: '取消',
onOk: () => resolve(true),
onCancel: () => resolve(false)
});
});
if (!confirmed) return;
logCleanupLoading.value = true;
const token = localStorage.getItem('token');
// 根据日志类型调用不同的API
let apiUrl = '';
let requestData = {};
if (logCleanupForm.logType === 'build') {
apiUrl = '/api/system/security/cleanup-build-logs/';
requestData = {
task_ids: logCleanupForm.selectedTasks,
days_before: logCleanupForm.daysBefore
};
} else if (logCleanupForm.logType === 'login') {
apiUrl = '/api/system/security/cleanup-login-logs/';
requestData = {
days_before: logCleanupForm.daysBefore
};
}
const response = await axios.post(apiUrl, requestData, {
headers: { 'Authorization': token }
});
if (response.data.code === 200) {
message.success(response.data.message);
if (logCleanupForm.logType === 'build') {
logCleanupForm.selectedTasks = [];
}
} else {
message.error(response.data.message || '日志清理失败');
}
} catch (error) {
console.error('Cleanup logs error:', error);
message.error('日志清理失败');
} finally {
logCleanupLoading.value = false;
}
};
onMounted(() => {
fetchSecurityConfig();
loadBuildTasksList();
});
</script>