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

View File

@@ -0,0 +1,56 @@
import jwt
import logging
from functools import wraps
from django.http import JsonResponse
from django.conf import settings
from ..models import UserToken
logger = logging.getLogger('apps')
def jwt_auth_required(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
token = request.headers.get('Authorization')
if not token:
logger.info('认证失败: 未提供Token')
return JsonResponse({
'code': 401,
'message': '未提供Token'
}, status=401)
try:
# 验证token是否存在于数据库
user_token = UserToken.objects.filter(token=token).first()
if not user_token:
logger.info('认证失败: Token无效')
return JsonResponse({
'code': 401,
'message': 'Token无效'
}, status=401)
# 解析token
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
request.user_id = payload.get('user_id')
return view_func(request, *args, **kwargs)
except jwt.ExpiredSignatureError:
logger.info('认证失败: Token已过期')
return JsonResponse({
'code': 401,
'message': 'Token已过期'
}, status=401)
except jwt.InvalidTokenError:
logger.info('认证失败: Token格式无效')
return JsonResponse({
'code': 401,
'message': '无效的Token'
}, status=401)
except Exception as e:
logger.error('认证过程发生错误', exc_info=True)
return JsonResponse({
'code': 500,
'message': '服务器错误'
}, status=500)
return wrapper

View File

@@ -0,0 +1,341 @@
import os
import subprocess
import logging
import time
import tempfile
from typing import List, Dict, Any, Callable
logger = logging.getLogger('apps')
class BuildStageExecutor:
"""构建阶段执行器"""
def __init__(self, build_path: str, send_log: Callable, record_time: Callable):
"""
初始化构建阶段执行器
Args:
build_path: 构建目录路径
send_log: 发送日志的回调函数
record_time: 记录时间的回调函数
"""
self.build_path = build_path
self.send_log = send_log
self.record_time = record_time
self.env = {} # 初始化为空字典,将由 Builder 设置
# 用于存储临时变量文件的路径
self.vars_file = os.path.join(self.build_path, '.build_vars')
self._init_vars_file()
def _init_vars_file(self):
"""初始化变量文件"""
try:
with open(self.vars_file, 'w') as f:
f.write('#!/bin/bash\n# 构建变量\n')
# 设置执行权限
os.chmod(self.vars_file, 0o755)
except Exception as e:
logger.error(f"初始化变量文件失败: {str(e)}", exc_info=True)
def _save_variables_to_file(self, variables):
"""
将变量保存到文件
Args:
variables: 变量字典
"""
try:
if not variables:
return
# 追加变量到变量文件
with open(self.vars_file, 'a') as f:
for name, value in variables.items():
if name.startswith(('_', 'BASH_', 'SHELL', 'HOME', 'PATH', 'PWD', 'OLDPWD')):
continue
safe_value = self._escape_shell_value(str(value))
f.write(f'export {name}={safe_value}\n')
except Exception as e:
logger.error(f"保存变量到文件失败: {str(e)}", exc_info=True)
def _escape_shell_value(self, value):
"""
转义shell变量值
Args:
value: 要转义的值
Returns:
str: 转义后的值
"""
try:
# 如果值为空,返回空字符串
if not value:
return '""'
# 如果值只包含安全字符(字母、数字、下划线、点、斜杠、冒号),不需要引号
import re
if re.match(r'^[a-zA-Z0-9_./:-]+$', value):
return value
# 用单引号包围,并转义其中的单引号
escaped_value = value.replace("'", "'\"'\"'")
return f"'{escaped_value}'"
except Exception as e:
logger.error(f"转义shell值失败: {str(e)}", exc_info=True)
return '""'
def _is_variable_assignment(self, line):
"""
检查是否是变量赋值语句
Args:
line: 要检查的行
Returns:
bool: 是否是变量赋值语句
"""
try:
import re
# 去除前导空格
line = line.strip()
# 检查是否是 export VAR=value 格式
if line.startswith('export '):
line = line[7:].strip() # 移除 'export ' 前缀
# 检查是否符合变量赋值格式VAR=value
# 变量名必须以字母或下划线开头,后面可以跟字母、数字、下划线
pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*='
return bool(re.match(pattern, line))
except Exception as e:
logger.error(f"检查变量赋值语句失败: {str(e)}", exc_info=True)
return False
def _create_temp_script_file(self, script_content: str, stage_name: str) -> str:
"""
创建临时脚本文件支持Jenkins风格的命令显示
Args:
script_content: 内联脚本内容
stage_name: 阶段名称
Returns:
str: 临时脚本文件路径
"""
try:
# 创建临时脚本文件
with tempfile.NamedTemporaryFile(
mode='w',
suffix='.sh',
prefix=f'build_stage_{stage_name}_',
dir=self.build_path,
delete=False
) as temp_file:
# 写入脚本头部
temp_file.write('#!/bin/bash\n')
temp_file.write('set -e # 遇到错误立即退出\n')
temp_file.write('set -o pipefail # 管道命令中任何一个失败都视为失败\n')
temp_file.write('\n')
# 加载变量文件
temp_file.write(f'# 加载构建变量\n')
temp_file.write(f'source "{self.vars_file}" 2>/dev/null || true\n')
temp_file.write('\n')
# 添加命令显示函数
temp_file.write('''# 命令显示函数
execute_with_display() {
echo "+ $*"
"$@"
}
''')
temp_file.write('# 用户脚本开始\n')
# 逐行处理脚本内容
for line in script_content.splitlines():
line = line.strip()
if line and not line.startswith('#'):
# 检查是否是变量赋值语句VAR=value 或 export VAR=value
if self._is_variable_assignment(line):
# 变量赋值:直接显示并执行
temp_file.write(f'echo "+ {line}"\n')
temp_file.write(f'{line}\n')
elif any(op in line for op in ['|', '>', '<', '&&', '||', ';']):
# 复杂命令先显示再在子shell中执行
temp_file.write(f'echo "+ {line}"\n')
temp_file.write(f'bash -c {repr(line)}\n')
else:
# 简单命令:使用函数显示并执行
temp_file.write(f'execute_with_display {line}\n')
elif line.startswith('#'):
# 保留注释
temp_file.write(f'{line}\n')
temp_file.write('\n')
script_path = temp_file.name
# 设置执行权限
os.chmod(script_path, 0o755)
return script_path
except Exception as e:
self.send_log(f"创建临时脚本文件失败: {str(e)}", stage_name)
return None
def _execute_script_unified(self, script_path: str, stage_name: str, check_termination: Callable = None) -> bool:
try:
# 检查是否应该终止
if check_termination and check_termination():
self.send_log("构建已被终止,跳过脚本执行", stage_name)
return False
# 执行脚本合并stdout和stderr到同一个流保持输出顺序
process = subprocess.Popen(
['/bin/bash', script_path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # 将stderr重定向到stdout保持输出顺序
cwd=self.build_path,
env=self.env,
universal_newlines=True,
bufsize=1 # 行缓冲,确保输出能够实时获取
)
# 实时读取并发送输出
import fcntl
import os
# 设置非阻塞模式
fd = process.stdout.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
# 持续读取直到进程结束
while process.poll() is None:
# 检查是否终止
if check_termination and check_termination():
process.terminate()
self.send_log("构建已被终止,停止当前脚本", stage_name)
return False
try:
line = process.stdout.readline()
if line:
line = line.rstrip()
self.send_log(line, stage_name, raw_output=True)
except BlockingIOError:
time.sleep(0.01)
continue
remaining_output = process.stdout.read()
if remaining_output:
for line in remaining_output.splitlines():
if line.strip():
self.send_log(line.rstrip(), stage_name, raw_output=True)
# 检查执行结果
success = process.returncode == 0
if not success:
self.send_log(f"脚本执行失败,返回码: {process.returncode}", stage_name)
return success
except Exception as e:
self.send_log(f"执行脚本时发生错误: {str(e)}", stage_name)
return False
finally:
if script_path.startswith(tempfile.gettempdir()) or '/build_stage_' in script_path:
try:
os.unlink(script_path)
except Exception as e:
logger.debug(f"清理临时脚本文件失败: {str(e)}")
def execute_stage(self, stage: Dict[str, Any], check_termination: Callable = None) -> bool:
"""
执行单个构建阶段
Args:
stage: 阶段配置信息
check_termination: 检查是否终止的回调函数
Returns:
bool: 执行是否成功
"""
try:
stage_name = stage.get('name', '未命名阶段')
if check_termination and check_termination():
self.send_log("构建已被终止,跳过此阶段", stage_name)
return False
# 记录阶段开始时间
stage_start_time = time.time()
# 执行脚本
success = self._execute_inline_script(stage, check_termination)
# 记录阶段耗时
stage_duration = time.time() - stage_start_time
self.record_time(stage_name, stage_start_time, stage_duration)
return success
except Exception as e:
self.send_log(f"执行阶段时发生错误: {str(e)}", stage_name)
return False
def _execute_inline_script(self, stage: Dict[str, Any], check_termination: Callable = None) -> bool:
"""
执行内联脚本
Args:
stage: 阶段配置信息
check_termination: 检查是否终止的回调函数
Returns:
bool: 执行是否成功
"""
stage_name = stage.get('name', '未命名阶段')
try:
script_content = stage.get('script', '').strip()
if not script_content:
self.send_log("脚本内容为空", stage_name)
return False
# 创建临时脚本文件
script_path = self._create_temp_script_file(script_content, stage_name)
if not script_path:
return False
# 脚本执行方法
success = self._execute_script_unified(script_path, stage_name, check_termination)
return success
except Exception as e:
self.send_log(f"执行内联脚本时发生错误: {str(e)}", stage_name)
return False
def execute_stages(self, stages: List[Dict[str, Any]], check_termination: Callable = None) -> bool:
"""
执行所有构建阶段
Args:
stages: 阶段配置列表
check_termination: 检查是否终止的回调函数
Returns:
bool: 所有阶段是否都执行成功
"""
if not stages:
self.send_log("没有配置构建阶段")
return False
for stage in stages:
if check_termination and check_termination():
self.send_log("构建已被终止,跳过后续阶段", "Build Stages")
return False
stage_name = stage.get('name', '未命名阶段')
self.send_log(f"开始执行阶段: {stage_name}", "Build Stages")
# 执行当前阶段
if not self.execute_stage(stage, check_termination):
self.send_log(f"阶段 {stage_name} 执行失败", "Build Stages")
return False
self.send_log(f"阶段 {stage_name} 执行完成", "Build Stages")
self.send_log("所有阶段执行完成", "Build Stages")
return True

View File

@@ -0,0 +1,578 @@
import os
import logging
import time
import subprocess
import tempfile
import re
import shutil
from datetime import datetime
from pathlib import Path
from django.conf import settings
from git import Repo
from git.exc import GitCommandError
from .build_stages import BuildStageExecutor
from .notifier import BuildNotifier
from .log_stream import log_stream_manager
from django.db.models import F
from ..models import BuildTask, BuildHistory
# from ..utils.builder import Builder
# from ..utils.crypto import decrypt_sensitive_data
logger = logging.getLogger('apps')
class Builder:
def __init__(self, task, build_number, commit_id, history):
self.task = task
self.build_number = build_number
self.commit_id = commit_id
self.history = history # 构建历史记录
self.log_buffer = [] # 缓存日志
# 检查是否已有指定的版本号
if self.history.version:
self.version = self.history.version
self.send_log(f"使用指定版本: {self.version}", "Version")
else:
# 为开发和测试环境生成新的版本号
self.version = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{commit_id[:8]}"
# 初始化构建时间信息
self.build_time = {
'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'stages_time': []
}
# 更新构建历史记录的状态和版本
self.history.status = 'running'
if not self.history.version: # 只有当没有版本号时才更新
self.history.version = self.version
self.history.build_time = self.build_time
self.history.save(update_fields=['status', 'version', 'build_time'])
# 设置构建目录
self.build_path = Path(settings.BUILD_ROOT) / task.name / self.version / task.project.name
# 创建实时日志流
log_stream_manager.create_build_stream(self.task.task_id, self.build_number)
def check_if_terminated(self):
"""检查构建是否已被终止"""
# 从数据库重新加载构建历史记录,以获取最新状态
try:
history_record = BuildHistory.objects.get(history_id=self.history.history_id)
if history_record.status == 'terminated':
# 如果状态为terminated构建已被手动终止
self.send_log("检测到构建已被手动终止,停止后续步骤", "System")
return True
return False
except Exception as e:
logger.error(f"检查构建状态时出错: {str(e)}", exc_info=True)
return False
def _filter_maven_progress(self, message):
# 只过滤Maven下载进度信息中的Progress()部分
if 'Progress (' in message and ('KB' in message or 'MB' in message or 'B/s' in message):
return None
# 过滤Maven下载进度条
if re.match(r'^Progress \(\d+\): .+', message.strip()):
return None
# 过滤空的进度行
if re.match(r'^\s*Progress\s*$', message.strip()):
return None
# 过滤下载进度百分比
if re.match(r'^\s*\d+%\s*$', message.strip()):
return None
return message
def send_log(self, message, stage=None, console_only=False, raw_output=False):
"""发送日志到缓存、实时流和控制台
Args:
message: 日志消息
stage: 阶段名称
console_only: 是否只输出到控制台(保留参数兼容性)
raw_output: 是否为原始输出(不添加阶段标记)
"""
# 过滤Maven Progress信息
filtered_message = self._filter_maven_progress(message)
if filtered_message is None:
return # 跳过被过滤的消息
# 格式化消息
if raw_output:
formatted_message = filtered_message
else:
formatted_message = filtered_message
# 如果有阶段名称,添加阶段标记
if stage:
formatted_message = f"[{stage}] {filtered_message}"
# 缓存日志
self.log_buffer.append(formatted_message)
# 推送到实时日志流
try:
log_stream_manager.push_log(
task_id=self.task.task_id,
build_number=self.build_number,
message=formatted_message + '\n',
stage=stage
)
except Exception as e:
# 降低日志级别,避免在清理阶段产生过多错误日志
if "日志队列不存在" in str(e):
logger.debug(f"日志队列已清理,跳过推送: {str(e)}")
else:
logger.error(f"推送实时日志失败: {str(e)}", exc_info=True)
# 批量更新数据库中的构建日志
try:
should_update_db = (
len(self.log_buffer) % 10 == 0 or # 每10条日志更新一次
not hasattr(self, '_last_db_update') or
time.time() - getattr(self, '_last_db_update', 0) >= 5 # 每5秒更新一次
)
if should_update_db:
current_log = '\n'.join(self.log_buffer)
self.history.build_log = current_log
self.history.save(update_fields=['build_log'])
self._last_db_update = time.time()
except Exception as e:
logger.error(f"批量更新构建日志失败: {str(e)}", exc_info=True)
# 输出到控制台 - 确保构建日志在控制台显示
logger.info(formatted_message, extra={
'from_builder': True, # 添加标记以区分构建日志
'task_id': self.task.task_id,
'build_number': self.build_number
})
def _save_build_log(self):
"""保存构建日志到历史记录"""
try:
self.history.build_log = '\n'.join(self.log_buffer)
self.history.save(update_fields=['build_log'])
except Exception as e:
logger.error(f"保存构建日志失败: {str(e)}", exc_info=True)
def clone_repository(self):
"""克隆Git仓库"""
try:
# 检查构建是否已被终止
if self.check_if_terminated():
return False
self.send_log("开始克隆代码...", "Git Clone")
self.send_log(f"构建目录: {self.build_path}", "Git Clone")
# 确保目录存在
self.build_path.parent.mkdir(parents=True, exist_ok=True)
# 获取Git凭证
repository = self.task.project.repository
self.send_log(f"仓库地址: {repository}", "Git Clone")
git_token = self.task.git_token.token if self.task.git_token else None
# 处理带有token的仓库URL
if git_token and repository.startswith('http'):
if '@' in repository:
repository = repository.split('@')[1]
repository = f'https://oauth2:{git_token}@{repository}'
else:
repository = repository.replace('://', f'://oauth2:{git_token}@')
# 使用构建历史记录中的分支
branch = self.history.branch
self.send_log(f"克隆分支: {branch}", "Git Clone")
self.send_log("正在克隆代码,请稍候...", "Git Clone")
# 克隆指定分支的代码
Repo.clone_from(
repository,
str(self.build_path),
branch=branch,
progress=self.git_progress
)
# 检查构建是否已被终止
if self.check_if_terminated():
return False
# 验证克隆是否成功
if not os.path.exists(self.build_path) or not os.listdir(self.build_path):
self.send_log("代码克隆失败:目录为空", "Git Clone")
return False
self.send_log("代码克隆完成", "Git Clone")
self.send_log(f"克隆目录验证成功: {self.build_path}", "Git Clone")
return True
except GitCommandError as e:
self.send_log(f"克隆代码失败: {str(e)}", "Git Clone")
return False
except Exception as e:
self.send_log(f"发生错误: {str(e)}", "Git Clone")
return False
def git_progress(self, op_code, cur_count, max_count=None, message=''):
"""Git进度回调"""
# 每秒检查一次构建是否已被终止
if int(time.time()) % 5 == 0: # 每5秒检查一次
if self.check_if_terminated():
# 如果构建已被终止尝试引发异常停止Git克隆
raise Exception("Build terminated")
pass
def clone_external_scripts(self):
"""克隆外部脚本库"""
try:
if not self.task.use_external_script:
return True
# 检查外部脚本库配置
config = self.task.external_script_config
if not config or not config.get('repo_url') or not config.get('directory') or not config.get('branch'):
self.send_log("外部脚本库配置不完整,跳过克隆", "External Scripts")
return True
# 检查构建是否已被终止
if self.check_if_terminated():
return False
repo_url = config.get('repo_url')
base_directory = config.get('directory')
branch = config.get('branch') # 分支为必填项
token_id = config.get('token_id')
# 从仓库URL中提取项目名称
import re
repo_name_match = re.search(r'/([^/]+?)(?:\.git)?/?$', repo_url)
if repo_name_match:
repo_name = repo_name_match.group(1)
if repo_name.endswith('.git'):
repo_name = repo_name[:-4]
else:
repo_name = 'external-scripts'
# 完整的克隆目录路径
directory = os.path.join(base_directory, repo_name)
self.send_log("开始克隆外部脚本库...", "External Scripts")
self.send_log(f"仓库地址: {repo_url}", "External Scripts")
self.send_log(f"基础目录: {base_directory}", "External Scripts")
self.send_log(f"项目名称: {repo_name}", "External Scripts")
self.send_log(f"完整目录: {directory}", "External Scripts")
self.send_log(f"分支: {branch}", "External Scripts")
# 获取Git Token如果配置了
git_token = None
if token_id:
try:
from ..models import GitlabTokenCredential
credential = GitlabTokenCredential.objects.get(credential_id=token_id)
git_token = credential.token
except:
self.send_log("获取Git Token失败尝试使用公开仓库方式克隆", "External Scripts")
# 处理带有token的仓库URL
if git_token and repo_url.startswith('http'):
if '@' in repo_url:
repo_url = repo_url.split('@')[1]
repo_url = f'https://oauth2:{git_token}@{repo_url}'
else:
repo_url = repo_url.replace('://', f'://oauth2:{git_token}@')
# 确保基础目录存在
os.makedirs(base_directory, exist_ok=True)
# 如果目标目录已存在且不为空,先清空
if os.path.exists(directory) and os.listdir(directory):
self.send_log(f"清空现有目录: {directory}", "External Scripts")
shutil.rmtree(directory)
# 克隆外部脚本库
self.send_log("正在克隆外部脚本库,请稍候...", "External Scripts")
from git import Repo
# 使用指定分支克隆
Repo.clone_from(
repo_url,
directory,
branch=branch
)
# 再次检查构建是否已被终止
if self.check_if_terminated():
return False
# 验证克隆是否成功
if not os.path.exists(directory) or not os.listdir(directory):
self.send_log("外部脚本库克隆失败:目录为空", "External Scripts")
return False
self.send_log("外部脚本库克隆完成", "External Scripts")
self.send_log(f"克隆目录验证成功: {directory}", "External Scripts")
return True
except Exception as e:
self.send_log(f"克隆外部脚本库失败: {str(e)}", "External Scripts")
# 如果用户配置了外部脚本库,克隆失败应该终止构建
self.send_log("外部脚本库克隆失败,终止构建", "External Scripts")
return False
def execute_stages(self, stage_executor):
"""执行构建阶段"""
try:
if not self.task.stages:
self.send_log("没有配置构建阶段", "Build Stages")
return False
# 检查构建是否已被终止
if self.check_if_terminated():
return False
# 执行所有阶段
success = stage_executor.execute_stages(self.task.stages, check_termination=self.check_if_terminated)
return success
except Exception as e:
self.send_log(f"执行构建阶段时发生错误: {str(e)}", "Build Stages")
return False
def execute(self):
"""执行构建"""
build_start_time = time.time()
success = False # 初始化成功状态
try:
# 在开始构建前检查构建是否已被终止
if self.check_if_terminated():
self._update_build_stats(False)
self._save_build_log()
return False
# 获取环境类型
environment_type = self.task.environment.type if self.task.environment else None
# 根据环境类型决定是否需要克隆代码
if environment_type in ['development', 'testing']:
# 只在开发和测试环境克隆代码
clone_start_time = time.time()
if not self.clone_repository():
self._update_build_stats(False) # 更新失败统计
self._update_build_time(build_start_time, False)
# 发送构建失败通知
notifier = BuildNotifier(self.history)
notifier.send_notifications()
return False
# 记录代码克隆阶段的时间
self.build_time['stages_time'].append({
'name': 'Git Clone',
'start_time': datetime.fromtimestamp(clone_start_time).strftime('%Y-%m-%d %H:%M:%S'),
'duration': str(int(time.time() - clone_start_time))
})
else:
self.send_log(f"预发布/生产环境构建,跳过代码克隆,直接使用版本: {self.version}", "Environment")
# 创建构建目录
os.makedirs(self.build_path, exist_ok=True)
# 再次检查构建是否已被终止
if self.check_if_terminated():
self._update_build_stats(False)
self._update_build_time(build_start_time, False)
return False
# 克隆外部脚本库(如果配置了)
external_script_start_time = time.time()
if not self.clone_external_scripts():
self._update_build_stats(False)
self._update_build_time(build_start_time, False)
# 发送构建失败通知
notifier = BuildNotifier(self.history)
notifier.send_notifications()
return False
# 记录外部脚本库克隆阶段的时间(如果启用了外部脚本库)
if self.task.use_external_script:
self.build_time['stages_time'].append({
'name': 'External Scripts Clone',
'start_time': datetime.fromtimestamp(external_script_start_time).strftime('%Y-%m-%d %H:%M:%S'),
'duration': str(int(time.time() - external_script_start_time))
})
# 检查构建是否已被终止
if self.check_if_terminated():
self._update_build_stats(False)
self._update_build_time(build_start_time, False)
return False
# 创建阶段执行器传递send_log方法和构建时间记录回调
stage_executor = BuildStageExecutor(
str(self.build_path),
lambda msg, stage=None, raw_output=False: self.send_log(msg, stage, raw_output=raw_output),
self._record_stage_time
)
# 设置系统内置环境变量
system_variables = {
# 编号相关变量
'BUILD_NUMBER': str(self.build_number),
'VERSION': self.version,
# Git相关变量
'COMMIT_ID': self.commit_id,
'BRANCH': self.history.branch,
# 项目相关变量
'PROJECT_NAME': self.task.project.name,
'PROJECT_ID': self.task.project.project_id,
'PROJECT_REPO': self.task.project.repository,
# 任务相关变量
'TASK_NAME': self.task.name,
'TASK_ID': self.task.task_id,
# 环境相关变量
'ENVIRONMENT': self.task.environment.name,
'ENVIRONMENT_TYPE': self.task.environment.type,
'ENVIRONMENT_ID': self.task.environment.environment_id,
# 别名(便于使用)
'service_name': self.task.name,
'build_env': self.task.environment.name,
'branch': self.history.branch,
'version': self.version,
# 构建路径
'BUILD_PATH': str(self.build_path),
'BUILD_WORKSPACE': str(self.build_path),
# Docker配置
'DOCKER_BUILDKIT': '0',
'BUILDKIT_PROGRESS': 'plain',
# Locale配置 - 使用稳定的POSIX locale避免SSH连接时的警告
'LC_ALL': 'POSIX',
'LANG': 'POSIX',
}
combined_env = {**os.environ, **system_variables}
stage_executor.env = combined_env
# 保存系统变量到文件
stage_executor._save_variables_to_file(system_variables)
# 执行构建阶段
success = self.execute_stages(stage_executor)
return success
except Exception as e:
self.send_log(f"构建过程中发生未捕获的异常: {str(e)}", "Error")
success = False
return False
finally:
# 更新构建统计和时间信息
self._update_build_stats(success)
self._update_build_time(build_start_time, success)
# 确保最终日志保存到数据库
self._save_build_log()
# 输出构建完成状态日志
self.history.refresh_from_db()
final_status = self.history.status
self.send_log(f"构建完成,状态: {final_status}", "Build")
# 确保构建完成状态日志也保存到数据库
self._save_build_log()
# 通知日志流管理器构建完成
try:
log_stream_manager.complete_build(
task_id=self.task.task_id,
build_number=self.build_number,
status=final_status
)
except Exception as e:
logger.error(f"通知日志流管理器构建完成失败: {str(e)}", exc_info=True)
# 发送构建通知
notifier = BuildNotifier(self.history)
notifier.send_notifications()
def _record_stage_time(self, stage_name: str, start_time: float, duration: float):
"""记录阶段执行时间
Args:
stage_name: 阶段名称
start_time: 开始时间戳
duration: 耗时(秒)
"""
stage_time = {
'name': stage_name,
'start_time': datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S'),
'duration': str(int(duration))
}
self.build_time['stages_time'].append(stage_time)
# 更新构建历史记录的阶段信息
self.history.stages = self.task.stages
self.history.save(update_fields=['stages'])
def _update_build_time(self, build_start_time: float, success: bool):
"""更新构建时间信息
Args:
build_start_time: 构建开始时间戳
success: 构建是否成功
"""
try:
# 计算总耗时
total_duration = int(time.time() - build_start_time)
# 更新构建时间信息
self.build_time['total_duration'] = str(total_duration)
self.build_time['end_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 检查当前构建状态如果已经是terminated则不覆盖状态
self.history.refresh_from_db()
if self.history.status != 'terminated':
# 只有在状态不是terminated时才更新状态
self.history.status = 'success' if success else 'failed'
self.history.build_time = self.build_time
self.history.save(update_fields=['status', 'build_time'])
except Exception as e:
logger.error(f"更新构建时间信息失败: {str(e)}", exc_info=True)
def _update_build_stats(self, success: bool):
"""更新构建统计信息
Args:
success: 构建是否成功
"""
try:
# 检查当前构建状态如果是terminated则不更新统计
self.history.refresh_from_db()
if self.history.status == 'terminated':
return
# 更新任务的构建统计信息
if success:
BuildTask.objects.filter(task_id=self.task.task_id).update(
success_builds=F('success_builds') + 1
)
# 只有成功的构建才更新版本号
BuildTask.objects.filter(task_id=self.task.task_id).update(
version=self.version
)
else:
BuildTask.objects.filter(task_id=self.task.task_id).update(
failure_builds=F('failure_builds') + 1
)
except Exception as e:
logger.error(f"更新构建统计信息失败: {str(e)}", exc_info=True)

View File

@@ -0,0 +1,194 @@
import threading
import queue
import time
import logging
from typing import Dict, Set, Optional
from dataclasses import dataclass
logger = logging.getLogger('apps')
@dataclass
class LogMessage:
"""日志消息数据类"""
task_id: str
build_number: int
message: str
stage: Optional[str] = None
timestamp: float = None
def __post_init__(self):
if self.timestamp is None:
self.timestamp = time.time()
class LogStreamManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'):
return
self._initialized = True
# 存储每个构建的日志队列 {(task_id, build_number): queue.Queue}
self._log_queues: Dict[tuple, queue.Queue] = {}
# 存储每个构建的SSE客户端集合 {(task_id, build_number): set}
self._sse_clients: Dict[tuple, Set] = {}
# 线程锁
self._queues_lock = threading.Lock()
self._clients_lock = threading.Lock()
logger.info("LogStreamManager initialized")
def get_build_key(self, task_id: str, build_number: int) -> tuple:
"""获取构建的唯一键"""
return (task_id, build_number)
def create_build_stream(self, task_id: str, build_number: int):
"""为新构建创建日志流"""
build_key = self.get_build_key(task_id, build_number)
with self._queues_lock:
if build_key not in self._log_queues:
self._log_queues[build_key] = queue.Queue(maxsize=20000) # 最大缓存20000条日志
logger.info(f"Created log stream for build {task_id}#{build_number}")
with self._clients_lock:
if build_key not in self._sse_clients:
self._sse_clients[build_key] = set()
def add_sse_client(self, task_id: str, build_number: int, client_id: str):
"""添加SSE客户端"""
build_key = self.get_build_key(task_id, build_number)
with self._clients_lock:
if build_key not in self._sse_clients:
self._sse_clients[build_key] = set()
self._sse_clients[build_key].add(client_id)
logger.info(f"Added SSE client {client_id} for build {task_id}#{build_number}")
def remove_sse_client(self, task_id: str, build_number: int, client_id: str):
"""移除SSE客户端"""
build_key = self.get_build_key(task_id, build_number)
with self._clients_lock:
if build_key in self._sse_clients:
self._sse_clients[build_key].discard(client_id)
logger.info(f"Removed SSE client {client_id} for build {task_id}#{build_number}")
if not self._sse_clients[build_key]:
del self._sse_clients[build_key]
self._cleanup_build_stream(build_key)
def _cleanup_build_stream(self, build_key: tuple):
"""清理构建流资源"""
with self._queues_lock:
if build_key in self._log_queues:
del self._log_queues[build_key]
logger.info(f"Cleaned up log stream for build {build_key[0]}#{build_key[1]}")
def push_log(self, task_id: str, build_number: int, message: str, stage: Optional[str] = None):
"""推送日志消息到流"""
build_key = self.get_build_key(task_id, build_number)
# 创建日志消息
log_msg = LogMessage(
task_id=task_id,
build_number=build_number,
message=message,
stage=stage
)
# 推送到队列
with self._queues_lock:
if build_key not in self._log_queues:
# 如果队列不存在,先创建
self._log_queues[build_key] = queue.Queue(maxsize=20000)
logger.debug(f"Created log queue for build {task_id}#{build_number} during push")
try:
# 非阻塞推送,如果队列满了就丢弃最老的消息
if self._log_queues[build_key].full():
try:
self._log_queues[build_key].get_nowait() # 移除最老的消息
except queue.Empty:
pass
self._log_queues[build_key].put_nowait(log_msg)
except queue.Full:
logger.warning(f"Log queue full for build {task_id}#{build_number}, dropping message")
def get_log_stream(self, task_id: str, build_number: int, client_id: str):
"""获取日志流生成器"""
build_key = self.get_build_key(task_id, build_number)
# 确保流存在
self.create_build_stream(task_id, build_number)
self.add_sse_client(task_id, build_number, client_id)
try:
while True:
try:
# 检查客户端是否还在连接
with self._clients_lock:
if build_key not in self._sse_clients or client_id not in self._sse_clients[build_key]:
logger.info(f"Client {client_id} disconnected from build {task_id}#{build_number}")
break
# 获取日志队列
with self._queues_lock:
log_queue = self._log_queues.get(build_key)
if log_queue is None:
break
try:
# 等待新日志消息超时时间为1秒
log_msg = log_queue.get(timeout=1.0)
yield log_msg
except queue.Empty:
# 超时,发送心跳
yield None # None表示心跳
except Exception as e:
logger.error(f"Error in log stream for {task_id}#{build_number}: {str(e)}")
break
finally:
# 清理客户端
self.remove_sse_client(task_id, build_number, client_id)
def complete_build(self, task_id: str, build_number: int, status: str):
"""标记构建完成"""
build_key = self.get_build_key(task_id, build_number)
# 推送完成消息
completion_msg = LogMessage(
task_id=task_id,
build_number=build_number,
message=f"BUILD_COMPLETE:{status}",
stage="SYSTEM"
)
with self._queues_lock:
if build_key in self._log_queues:
try:
self._log_queues[build_key].put_nowait(completion_msg)
except queue.Full:
pass
def has_active_clients(self, task_id: str, build_number: int) -> bool:
"""检查是否有活跃的SSE客户端"""
build_key = self.get_build_key(task_id, build_number)
with self._clients_lock:
return build_key in self._sse_clients and len(self._sse_clients[build_key]) > 0
# 全局单例实例
log_stream_manager = LogStreamManager()

View File

@@ -0,0 +1,252 @@
import json
import time
import hmac
import base64
import hashlib
import logging
import requests
from urllib.parse import quote_plus
from django.conf import settings
from ..models import NotificationRobot, BuildHistory
logger = logging.getLogger('apps')
class BuildNotifier:
"""构建通知工具类"""
def __init__(self, history: BuildHistory):
self.history = history
self.task = history.task
self.project = history.task.project
self.environment = history.task.environment
def _sign_dingtalk(self, secret: str, timestamp: str) -> str:
"""钉钉机器人签名"""
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: str, timestamp: str) -> str:
"""飞书机器人签名"""
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 _get_build_status_emoji(self) -> str:
"""获取构建状态对应的emoji"""
status_emoji = {
'success': '',
'failed': '',
'running': '🔄',
'pending': '',
'terminated': '🛑'
}
return status_emoji.get(self.history.status, '')
def _get_duration_text(self) -> str:
"""获取构建耗时文本"""
if not self.history.build_time or 'total_duration' not in self.history.build_time:
return '未完成'
duration = int(self.history.build_time['total_duration'])
if duration < 60:
return f'{duration}'
minutes = duration // 60
seconds = duration % 60
return f'{minutes}{seconds}'
def _get_status_text(self) -> str:
"""获取状态文本"""
status_texts = {
'success': '构建成功',
'failed': '构建失败',
'running': '构建中',
'pending': '等待中',
'terminated': '构建已终止'
}
return status_texts.get(self.history.status, self.history.status)
def _get_build_url(self) -> str:
"""获取构建历史页面URL"""
base_url = getattr(settings, 'WEB_URL', 'http://localhost:5173') # 如果未配置,使用默认值
return f"{base_url}/build/history?history_id={self.history.history_id}"
def _format_dingtalk_message(self) -> dict:
"""格式化钉钉通知消息"""
status_text = self._get_status_text()
build_url = self._get_build_url()
content = [
f"## 🔔 构建通知:{status_text}",
"---",
"**构建详情:**",
f"- **任务名称**{self.task.name}",
f"- **构建编号**#{self.history.build_number}",
f"- **构建版本**{self.history.version}",
f"- **构建分支**{self.history.branch}",
f"- **提交ID**{self.history.commit_id[:8] if self.history.commit_id else ''}",
f"- **构建环境**{self.environment.name}",
f"- **构建人员**{self.history.operator.name if self.history.operator else '系统'}",
f"- **构建耗时**{self._get_duration_text()}",
"",
"**构建需求:**",
f"> {self.history.requirement or ''}",
"",
f"**查看详情:**[点击查看构建日志]({build_url})",
"",
"---",
"**注意事项:**",
"1. 此为自动通知,请勿回复",
"2. 如遇构建失败,请先查看构建日志进行排查",
"3. 如需帮助,请联系运维同学"
]
return {
"msgtype": "markdown",
"markdown": {
"title": f"{status_text}: {self.task.name} #{self.history.build_number}",
"text": "\n".join(content)
},
"at": {
"isAtAll": True
}
}
def _format_wecom_message(self) -> dict:
"""格式化企业微信通知消息"""
status_text = self._get_status_text()
build_url = self._get_build_url()
content = [
f"## 🔔 构建通知:{status_text}",
"---",
"@all", # 企业微信使用 @all 来@所有人
"",
"**构建详情:**",
f"- **任务名称**{self.task.name}",
f"- **构建编号**#{self.history.build_number}",
f"- **构建版本**{self.history.version}",
f"- **构建分支**{self.history.branch}",
f"- **提交ID**{self.history.commit_id[:8] if self.history.commit_id else ''}",
f"- **构建环境**{self.environment.name}",
f"- **构建人员**{self.history.operator.name if self.history.operator else '系统'}",
f"- **构建耗时**{self._get_duration_text()}",
"",
"**构建需求:**",
f"> {self.history.requirement or ''}",
"",
f"**查看详情:**[点击查看构建日志]({build_url})",
"",
"---",
"**注意事项:**",
"1. 此为自动通知,请勿回复",
"2. 如遇构建失败,请先查看构建日志进行排查",
"3. 如需帮助,请联系运维同学"
]
return {
"msgtype": "markdown",
"markdown": {
"content": "\n".join(content)
}
}
def _format_feishu_message(self) -> dict:
"""格式化飞书通知消息"""
status_text = self._get_status_text()
build_url = self._get_build_url()
content = [
f"🔔 构建通知:{status_text}",
"---",
"<at user_id=\"all\">所有人</at>", # 飞书使用这种格式@所有人
"",
"**构建详情:**",
f"- **任务名称**{self.task.name}",
f"- **构建编号**#{self.history.build_number}",
f"- **构建版本**{self.history.version}",
f"- **构建分支**{self.history.branch}",
f"- **提交ID**{self.history.commit_id[:8] if self.history.commit_id else ''}",
f"- **构建环境**{self.environment.name}",
f"- **构建人员**{self.history.operator.name if self.history.operator else '系统'}",
f"- **构建耗时**{self._get_duration_text()}",
"",
"**构建需求:**",
f"> {self.history.requirement or ''}",
"",
f"**查看详情:**[点击查看构建日志]({build_url})",
"",
"---",
"**注意事项:**",
"1. 此为自动通知,请勿回复",
"2. 如遇构建失败,请先查看构建日志进行排查",
"3. 如需帮助,请联系运维同学"
]
return {
"msg_type": "text",
"content": {
"text": "\n".join(content)
}
}
def send_notifications(self):
"""发送构建通知"""
if not self.task.notification_channels:
logger.info(f"任务 {self.task.name} 未配置通知方式")
return
# 获取需要通知的机器人
robots = NotificationRobot.objects.filter(robot_id__in=self.task.notification_channels)
for robot in robots:
try:
webhook = robot.webhook
timestamp = str(int(time.time() * 1000))
headers = {}
# 根据机器人类型处理安全设置
if robot.security_type == 'secret' and robot.secret:
if robot.type == 'dingtalk':
sign = self._sign_dingtalk(robot.secret, timestamp)
webhook = f"{webhook}&timestamp={timestamp}&sign={quote_plus(sign)}"
elif robot.type == 'feishu':
sign = self._sign_feishu(robot.secret, timestamp)
headers.update({
"X-Timestamp": timestamp,
"X-Sign": sign
})
# 根据机器人类型获取消息内容
if robot.type == 'dingtalk':
message = self._format_dingtalk_message()
elif robot.type == 'wecom':
message = self._format_wecom_message()
elif robot.type == 'feishu':
message = self._format_feishu_message()
else:
logger.error(f"不支持的机器人类型: {robot.type}")
continue
# 发送通知
response = requests.post(webhook, json=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:
logger.info(f"发送 {robot.type} 通知成功: {robot.name}")
else:
logger.error(f"发送 {robot.type} 通知失败: {response.text}")
else:
logger.error(f"发送 {robot.type} 通知失败: {response.text}")
except Exception as e:
logger.error(f"发送 {robot.type} 通知出错: {str(e)}", exc_info=True)

View File

@@ -0,0 +1,128 @@
import json
import logging
from ..models import User, UserRole
logger = logging.getLogger('apps')
def get_user_permissions(user_id):
"""
获取用户的权限信息
Args:
user_id: 用户ID
Returns:
dict: 用户权限信息,包含菜单权限、功能权限和数据权限
"""
try:
# 获取用户信息
try:
user = User.objects.get(user_id=user_id)
except User.DoesNotExist:
logger.error(f'获取用户权限时用户不存在: {user_id}')
return {
'menu': [],
'function': {},
'data': {
'project_scope': 'all',
'project_ids': [],
'environment_scope': 'all',
'environment_types': []
}
}
# 获取用户的所有角色
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:
permissions = {}
# 合并菜单权限
if permissions.get('menu'):
menu_permissions.update(permissions['menu'])
# 合并功能权限
if permissions.get('function'):
for module, actions in permissions['function'].items():
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'):
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'] = []
return {
'menu': list(menu_permissions),
'function': function_permissions,
'data': data_permissions
}
except Exception as e:
logger.error(f'获取用户权限失败: {str(e)}', exc_info=True)
# 返回默认的空权限
return {
'menu': [],
'function': {},
'data': {
'project_scope': 'all',
'project_ids': [],
'environment_scope': 'all',
'environment_types': []
}
}

View File

@@ -0,0 +1,214 @@
import re
import hashlib
import logging
from datetime import datetime, timedelta
from django.utils import timezone
from ..models import SecurityConfig, LoginAttempt, User
logger = logging.getLogger('apps')
class SecurityValidator:
"""安全验证工具类"""
@staticmethod
def get_security_config():
"""获取安全配置"""
try:
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 config
except Exception as e:
logger.error(f'获取安全配置失败: {str(e)}')
# 返回默认配置
return type('SecurityConfig', (), {
'min_password_length': 8,
'password_complexity': ['lowercase', 'number'],
'session_timeout': 120,
'max_login_attempts': 5,
'lockout_duration': 30,
'enable_2fa': False
})()
@staticmethod
def validate_password(password):
"""验证密码是否符合安全策略"""
config = SecurityValidator.get_security_config()
# 检查密码长度
if len(password) < config.min_password_length:
return False, f'密码长度不能少于{config.min_password_length}'
# 检查密码复杂度
complexity_checks = {
'uppercase': (r'[A-Z]', '大写字母'),
'lowercase': (r'[a-z]', '小写字母'),
'number': (r'[0-9]', '数字'),
'special': (r'[!@#$%^&*(),.?":{}|<>]', '特殊字符')
}
missing_requirements = []
for requirement in config.password_complexity:
if requirement in complexity_checks:
pattern, description = complexity_checks[requirement]
if not re.search(pattern, password):
missing_requirements.append(description)
if missing_requirements:
return False, f'密码必须包含: {", ".join(missing_requirements)}'
return True, '密码验证通过'
@staticmethod
def check_account_lockout(user, ip_address):
"""检查账户是否被锁定"""
try:
# 首先检查用户表中的status字段
if user.status == 0:
return False, '账户已被锁定,请联系管理员解锁'
config = SecurityValidator.get_security_config()
# 获取登录尝试记录
try:
attempt = LoginAttempt.objects.get(user=user, ip_address=ip_address)
except LoginAttempt.DoesNotExist:
return True, '账户未被锁定'
# 如果账户被锁定,检查是否已过期
if attempt.locked_until and attempt.locked_until > timezone.now():
remaining_time = attempt.locked_until - timezone.now()
minutes = int(remaining_time.total_seconds() / 60)
return False, f'账户因登录失败次数过多被临时锁定,请在{minutes}分钟后重试'
# 如果临时锁定已过期,重置失败次数并解锁
if attempt.locked_until and attempt.locked_until <= timezone.now():
attempt.failed_attempts = 0
attempt.locked_until = None
attempt.save()
# 如果用户被系统锁定status=0检查是否需要自动解锁
if user.status == 0:
# 暂时保持锁定状态,需要管理员手动解锁
return False, '账户已被锁定,请联系管理员解锁'
return True, '账户未被锁定'
except Exception as e:
logger.error(f'检查账户锁定状态失败: {str(e)}')
return True, '锁定检查跳过'
@staticmethod
def record_failed_login(user, ip_address):
"""记录登录失败"""
try:
config = SecurityValidator.get_security_config()
# 获取或创建登录尝试记录
attempt, created = LoginAttempt.objects.get_or_create(
user=user,
ip_address=ip_address,
defaults={'failed_attempts': 0}
)
# 增加失败次数
attempt.failed_attempts += 1
attempt.last_attempt_time = timezone.now()
# 如果达到最大失败次数,锁定账户
if attempt.failed_attempts >= config.max_login_attempts:
attempt.locked_until = timezone.now() + timedelta(minutes=config.lockout_duration)
# 同时将用户状态设置为锁定
user.status = 0
user.save()
logger.warning(f'用户[{user.username}]账户因多次登录失败被锁定IP: {ip_address}')
attempt.save()
return attempt.failed_attempts, config.max_login_attempts
except Exception as e:
logger.error(f'记录登录失败失败: {str(e)}')
return 0, 5
@staticmethod
def record_successful_login(user, ip_address):
"""记录登录成功,重置失败次数"""
try:
# 清除登录失败记录
LoginAttempt.objects.filter(
user=user,
ip_address=ip_address
).delete()
except Exception as e:
logger.error(f'重置登录失败记录失败: {str(e)}')
@staticmethod
def unlock_user_account(user):
"""解锁用户账户(管理员操作)"""
try:
# 清除所有登录尝试记录
LoginAttempt.objects.filter(user=user).delete()
# 解锁用户状态
if user.status == 0:
user.status = 1
user.save()
logger.info(f'管理员解锁了用户[{user.username}]的账户')
return True, '账户解锁成功'
return True, '账户状态正常'
except Exception as e:
logger.error(f'解锁用户账户失败: {str(e)}')
return False, '解锁失败'
@staticmethod
def lock_user_account(user):
"""锁定用户账户(管理员操作)"""
try:
# 锁定用户状态
if user.status == 1:
user.status = 0
user.save()
logger.info(f'管理员锁定了用户[{user.username}]的账户')
return True, '账户锁定成功'
return True, '账户已处于锁定状态'
except Exception as e:
logger.error(f'锁定用户账户失败: {str(e)}')
return False, '锁定失败'
@staticmethod
def is_session_expired(login_time):
"""检查会话是否过期"""
try:
config = SecurityValidator.get_security_config()
if not login_time:
return True
# 计算会话过期时间
session_expire_time = login_time + timedelta(minutes=config.session_timeout)
return timezone.now() > session_expire_time
except Exception as e:
logger.error(f'检查会话过期失败: {str(e)}')
return False
def validate_password_strength(password):
"""独立的密码强度验证函数,供其他模块使用"""
return SecurityValidator.validate_password(password)