2025-11-26 21:11:12 +08:00
|
|
|
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:
|
|
|
|
|
"""
|
2025-11-28 01:11:21 +08:00
|
|
|
Retrieve projects for current user.
|
2025-11-26 21:11:12 +08:00
|
|
|
"""
|
|
|
|
|
query = select(Project).options(selectinload(Project.owner))
|
2025-11-28 01:11:21 +08:00
|
|
|
# 只返回当前用户的项目
|
|
|
|
|
query = query.where(Project.owner_id == current_user.id)
|
2025-11-26 21:11:12 +08:00
|
|
|
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:
|
|
|
|
|
"""
|
2025-11-28 01:11:21 +08:00
|
|
|
Get statistics for current user.
|
2025-11-26 21:11:12 +08:00
|
|
|
"""
|
2025-11-28 01:11:21 +08:00
|
|
|
# 只统计当前用户的项目
|
|
|
|
|
projects_result = await db.execute(
|
|
|
|
|
select(Project).where(Project.owner_id == current_user.id)
|
|
|
|
|
)
|
2025-11-26 21:11:12 +08:00
|
|
|
projects = projects_result.scalars().all()
|
2025-11-28 01:11:21 +08:00
|
|
|
project_ids = [p.id for p in projects]
|
2025-11-26 21:11:12 +08:00
|
|
|
|
2025-11-28 01:11:21 +08:00
|
|
|
# 只统计当前用户项目的任务
|
|
|
|
|
tasks_result = await db.execute(
|
|
|
|
|
select(AuditTask).where(AuditTask.project_id.in_(project_ids)) if project_ids else select(AuditTask).where(False)
|
|
|
|
|
)
|
2025-11-26 21:11:12 +08:00
|
|
|
tasks = tasks_result.scalars().all()
|
2025-11-28 01:11:21 +08:00
|
|
|
task_ids = [t.id for t in tasks]
|
2025-11-26 21:11:12 +08:00
|
|
|
|
2025-11-28 01:11:21 +08:00
|
|
|
# 只统计当前用户任务的问题
|
|
|
|
|
issues_result = await db.execute(
|
|
|
|
|
select(AuditIssue).where(AuditIssue.task_id.in_(task_ids)) if task_ids else select(AuditIssue).where(False)
|
|
|
|
|
)
|
2025-11-26 21:11:12 +08:00
|
|
|
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"}
|