""" 数据库服务 实现项目映射、分支模式匹配等功能 """ import asyncio from typing import Optional, List, Dict, Any from datetime import datetime import structlog import re from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.sql import text from app.config import get_settings logger = structlog.get_logger() Base = declarative_base() # 数据库模型 class APIKey(Base): """API 密钥模型""" __tablename__ = "api_keys" id = Column(Integer, primary_key=True, autoincrement=True) key = Column(String(255), unique=True, nullable=False) description = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class ProjectMapping(Base): """项目映射模型""" __tablename__ = "project_mappings" id = Column(Integer, primary_key=True, autoincrement=True) repository_name = Column(String(255), unique=True, nullable=False) default_job = Column(String(255)) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关系 branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan") branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan") class BranchJob(Base): """分支任务映射模型""" __tablename__ = "branch_jobs" id = Column(Integer, primary_key=True, autoincrement=True) project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) branch_name = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关系 project = relationship("ProjectMapping", back_populates="branch_jobs") class BranchPattern(Base): """分支模式映射模型""" __tablename__ = "branch_patterns" id = Column(Integer, primary_key=True, autoincrement=True) project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) pattern = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关系 project = relationship("ProjectMapping", back_populates="branch_patterns") class TriggerLog(Base): """触发日志模型""" __tablename__ = "trigger_logs" id = Column(Integer, primary_key=True, autoincrement=True) repository_name = Column(String(255), nullable=False) branch_name = Column(String(255), nullable=False) commit_sha = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) status = Column(String(50), nullable=False) error_message = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) class DatabaseService: """数据库服务""" def __init__(self): self.settings = get_settings() self.engine = None self.SessionLocal = None self._init_database() def _init_database(self): """初始化数据库""" try: self.engine = create_engine( self.settings.database.url, echo=self.settings.database.echo, pool_size=self.settings.database.pool_size, max_overflow=self.settings.database.max_overflow ) # 创建表 Base.metadata.create_all(bind=self.engine) # 创建会话工厂 self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) logger.info("Database initialized successfully") except Exception as e: logger.error("Failed to initialize database", error=str(e)) raise def get_session(self): """获取数据库会话""" return self.SessionLocal() async def get_project_mapping(self, repository_name: str) -> Optional[Dict[str, Any]]: """ 获取项目映射 Args: repository_name: 仓库名 Returns: Dict: 项目映射信息 """ try: def _get_mapping(): session = self.get_session() try: project = session.query(ProjectMapping).filter( ProjectMapping.repository_name == repository_name ).first() if not project: return None # 构建返回数据 result = { "id": project.id, "repository_name": project.repository_name, "default_job": project.default_job, "branch_jobs": [], "branch_patterns": [] } # 添加分支任务映射 for branch_job in project.branch_jobs: result["branch_jobs"].append({ "id": branch_job.id, "branch_name": branch_job.branch_name, "job_name": branch_job.job_name }) # 添加分支模式映射 for pattern in project.branch_patterns: result["branch_patterns"].append({ "id": pattern.id, "pattern": pattern.pattern, "job_name": pattern.job_name }) return result finally: session.close() # 在线程池中执行数据库操作 loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _get_mapping) except Exception as e: logger.error("Failed to get project mapping", repository_name=repository_name, error=str(e)) return None async def determine_job_name(self, repository_name: str, branch_name: str) -> Optional[str]: """ 根据分支名确定任务名 Args: repository_name: 仓库名 branch_name: 分支名 Returns: str: 任务名 """ try: project = await self.get_project_mapping(repository_name) if not project: return None # 1. 检查精确分支匹配 for branch_job in project["branch_jobs"]: if branch_job["branch_name"] == branch_name: logger.debug("Found exact branch match", branch=branch_name, job=branch_job["job_name"]) return branch_job["job_name"] # 2. 检查模式匹配 for pattern in project["branch_patterns"]: try: if re.match(pattern["pattern"], branch_name): logger.debug("Branch matched pattern", branch=branch_name, pattern=pattern["pattern"], job=pattern["job_name"]) return pattern["job_name"] except re.error as e: logger.error("Invalid regex pattern", pattern=pattern["pattern"], error=str(e)) continue # 3. 使用默认任务 if project["default_job"]: logger.debug("Using default job", branch=branch_name, job=project["default_job"]) return project["default_job"] return None except Exception as e: logger.error("Failed to determine job name", repository_name=repository_name, branch_name=branch_name, error=str(e)) return None async def log_trigger(self, log_data: Dict[str, Any]) -> bool: """ 记录触发日志 Args: log_data: 日志数据 Returns: bool: 是否成功 """ try: def _log_trigger(): session = self.get_session() try: log = TriggerLog( repository_name=log_data["repository_name"], branch_name=log_data["branch_name"], commit_sha=log_data["commit_sha"], job_name=log_data["job_name"], status=log_data["status"], error_message=log_data.get("error_message") ) session.add(log) session.commit() return True except Exception as e: session.rollback() logger.error("Failed to log trigger", error=str(e)) return False finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _log_trigger) except Exception as e: logger.error("Failed to log trigger", error=str(e)) return False async def get_trigger_logs(self, repository_name: str = None, branch_name: str = None, limit: int = 100) -> List[Dict[str, Any]]: """ 获取触发日志 Args: repository_name: 仓库名(可选) branch_name: 分支名(可选) limit: 限制数量 Returns: List: 日志列表 """ try: def _get_logs(): session = self.get_session() try: query = session.query(TriggerLog) if repository_name: query = query.filter(TriggerLog.repository_name == repository_name) if branch_name: query = query.filter(TriggerLog.branch_name == branch_name) logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() return [ { "id": log.id, "repository_name": log.repository_name, "branch_name": log.branch_name, "commit_sha": log.commit_sha, "job_name": log.job_name, "status": log.status, "error_message": log.error_message, "created_at": log.created_at.isoformat() } for log in logs ] finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _get_logs) except Exception as e: logger.error("Failed to get trigger logs", error=str(e)) return [] async def create_project_mapping(self, mapping_data: Dict[str, Any]) -> bool: """ 创建项目映射 Args: mapping_data: 映射数据 Returns: bool: 是否成功 """ try: def _create_mapping(): session = self.get_session() try: # 创建项目映射 project = ProjectMapping( repository_name=mapping_data["repository_name"], default_job=mapping_data.get("default_job") ) session.add(project) session.flush() # 获取 ID # 添加分支任务映射 for branch_job in mapping_data.get("branch_jobs", []): job = BranchJob( project_id=project.id, branch_name=branch_job["branch_name"], job_name=branch_job["job_name"] ) session.add(job) # 添加分支模式映射 for pattern in mapping_data.get("branch_patterns", []): pattern_obj = BranchPattern( project_id=project.id, pattern=pattern["pattern"], job_name=pattern["job_name"] ) session.add(pattern_obj) session.commit() return True except Exception as e: session.rollback() logger.error("Failed to create project mapping", error=str(e)) return False finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _create_mapping) except Exception as e: logger.error("Failed to create project mapping", error=str(e)) return False # 全局数据库服务实例 _database_service: Optional[DatabaseService] = None def get_database_service() -> DatabaseService: """获取数据库服务实例""" global _database_service if _database_service is None: _database_service = DatabaseService() return _database_service