from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import selectinload from pydantic import BaseModel from datetime import datetime from app.api import deps from app.db.session import get_db, AsyncSessionLocal from app.models.project import Project from app.models.user import User from app.models.audit import AuditTask, AuditIssue from app.models.user_config import UserConfig from app.services.scanner import scan_repo_task router = APIRouter() # Schemas class ProjectCreate(BaseModel): name: str repository_url: Optional[str] = None repository_type: Optional[str] = "other" description: Optional[str] = None default_branch: Optional[str] = "main" programming_languages: Optional[List[str]] = None class ProjectUpdate(BaseModel): name: Optional[str] = None repository_url: Optional[str] = None repository_type: Optional[str] = None description: Optional[str] = None default_branch: Optional[str] = None programming_languages: Optional[List[str]] = None class OwnerSchema(BaseModel): id: str email: Optional[str] = None full_name: Optional[str] = None avatar_url: Optional[str] = None role: Optional[str] = None class Config: from_attributes = True class ProjectResponse(BaseModel): id: str name: str description: Optional[str] = None repository_url: Optional[str] = None repository_type: Optional[str] = None default_branch: Optional[str] = None programming_languages: Optional[str] = None owner_id: str is_active: bool created_at: datetime updated_at: Optional[datetime] = None owner: Optional[OwnerSchema] = None class Config: from_attributes = True class StatsResponse(BaseModel): total_projects: int active_projects: int total_tasks: int completed_tasks: int total_issues: int resolved_issues: int @router.post("/", response_model=ProjectResponse) async def create_project( *, db: AsyncSession = Depends(get_db), project_in: ProjectCreate, current_user: User = Depends(deps.get_current_user), ) -> Any: """ Create new project. """ import json project = Project( name=project_in.name, repository_url=project_in.repository_url, repository_type=project_in.repository_type or "other", description=project_in.description, default_branch=project_in.default_branch or "main", programming_languages=json.dumps(project_in.programming_languages or []), owner_id=current_user.id ) db.add(project) await db.commit() await db.refresh(project) return project @router.get("/", response_model=List[ProjectResponse]) async def read_projects( db: AsyncSession = Depends(get_db), skip: int = 0, limit: int = 100, include_deleted: bool = False, current_user: User = Depends(deps.get_current_user), ) -> Any: """ Retrieve projects. """ query = select(Project).options(selectinload(Project.owner)) if not include_deleted: query = query.where(Project.is_active == True) query = query.order_by(Project.created_at.desc()).offset(skip).limit(limit) result = await db.execute(query) return result.scalars().all() @router.get("/deleted", response_model=List[ProjectResponse]) async def read_deleted_projects( db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Retrieve deleted (soft-deleted) projects for current user. """ result = await db.execute( select(Project) .options(selectinload(Project.owner)) .where(Project.owner_id == current_user.id) .where(Project.is_active == False) .order_by(Project.updated_at.desc()) ) return result.scalars().all() @router.get("/stats", response_model=StatsResponse) async def get_stats( db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Get overall statistics. """ projects_result = await db.execute(select(Project)) projects = projects_result.scalars().all() tasks_result = await db.execute(select(AuditTask)) tasks = tasks_result.scalars().all() issues_result = await db.execute(select(AuditIssue)) issues = issues_result.scalars().all() return { "total_projects": len(projects), "active_projects": len([p for p in projects if p.is_active]), "total_tasks": len(tasks), "completed_tasks": len([t for t in tasks if t.status == "completed"]), "total_issues": len(issues), "resolved_issues": len([i for i in issues if i.status == "resolved"]), } @router.get("/{id}", response_model=ProjectResponse) async def read_project( id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Get project by ID. """ result = await db.execute( select(Project) .options(selectinload(Project.owner)) .where(Project.id == id) ) project = result.scalars().first() if not project: raise HTTPException(status_code=404, detail="项目不存在") return project @router.put("/{id}", response_model=ProjectResponse) async def update_project( id: str, *, db: AsyncSession = Depends(get_db), project_in: ProjectUpdate, current_user: User = Depends(deps.get_current_user), ) -> Any: """ Update project. """ import json result = await db.execute(select(Project).where(Project.id == id)) project = result.scalars().first() if not project: raise HTTPException(status_code=404, detail="项目不存在") update_data = project_in.model_dump(exclude_unset=True) if "programming_languages" in update_data and update_data["programming_languages"] is not None: update_data["programming_languages"] = json.dumps(update_data["programming_languages"]) for field, value in update_data.items(): setattr(project, field, value) project.updated_at = datetime.utcnow() await db.commit() await db.refresh(project) return project @router.delete("/{id}") async def delete_project( id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Soft delete project. """ result = await db.execute(select(Project).where(Project.id == id)) project = result.scalars().first() if not project: raise HTTPException(status_code=404, detail="项目不存在") # 检查权限:只有项目所有者可以删除 if project.owner_id != current_user.id: raise HTTPException(status_code=403, detail="无权删除此项目") project.is_active = False project.updated_at = datetime.utcnow() await db.commit() return {"message": "项目已删除"} @router.post("/{id}/restore") async def restore_project( id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Restore soft-deleted project. """ result = await db.execute(select(Project).where(Project.id == id)) project = result.scalars().first() if not project: raise HTTPException(status_code=404, detail="项目不存在") # 检查权限:只有项目所有者可以恢复 if project.owner_id != current_user.id: raise HTTPException(status_code=403, detail="无权恢复此项目") project.is_active = True project.updated_at = datetime.utcnow() await db.commit() return {"message": "项目已恢复"} @router.delete("/{id}/permanent") async def permanently_delete_project( id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Permanently delete project. """ result = await db.execute(select(Project).where(Project.id == id)) project = result.scalars().first() if not project: raise HTTPException(status_code=404, detail="项目不存在") # 检查权限:只有项目所有者可以永久删除 if project.owner_id != current_user.id: raise HTTPException(status_code=403, detail="无权永久删除此项目") await db.delete(project) await db.commit() return {"message": "项目已永久删除"} @router.post("/{id}/scan") async def scan_project( id: str, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), current_user: User = Depends(deps.get_current_user), ) -> Any: """ Start a scan task. """ project = await db.get(Project, id) if not project: raise HTTPException(status_code=404, detail="项目不存在") # Create Task Record task = AuditTask( project_id=project.id, created_by=current_user.id, task_type="repository", status="pending" ) db.add(task) await db.commit() await db.refresh(task) # 获取用户配置 from sqlalchemy.future import select import json result = await db.execute( select(UserConfig).where(UserConfig.user_id == current_user.id) ) config = result.scalar_one_or_none() user_config = {} if config: user_config = { 'llmConfig': json.loads(config.llm_config) if config.llm_config else {}, 'otherConfig': json.loads(config.other_config) if config.other_config else {}, } # Trigger Background Task background_tasks.add_task(scan_repo_task, task.id, AsyncSessionLocal, user_config) return {"task_id": task.id, "status": "started"}