"""项目管理路由""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func as sa_func from typing import List, Optional from datetime import datetime from database import get_db from models import ( User, Project, Submission, ProjectType, ProjectStatus, PhaseGroup, WorkType, ProjectMilestone, DEFAULT_MILESTONES ) from schemas import ( ProjectCreate, ProjectUpdate, ProjectOut, MilestoneOut, MilestoneCreate ) from auth import get_current_user, require_permission router = APIRouter(prefix="/api/projects", tags=["项目管理"]) def enrich_project(p: Project, db: Session) -> ProjectOut: """将项目对象转为带计算字段的输出""" # 累计提交秒数(仅有秒数的提交) total_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.project_id == p.id, Submission.total_seconds > 0 ).scalar() or 0 target = p.target_total_seconds progress = round(total_secs / target * 100, 1) if target > 0 else 0 # 损耗 = 测试损耗 + 超产损耗(排除测试秒数避免双重计数) test_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.project_id == p.id, Submission.work_type == WorkType.TEST ).scalar() or 0 production_secs = total_secs - test_secs overproduction = max(0, production_secs - target) waste = test_secs + overproduction waste_rate = round(waste / target * 100, 1) if target > 0 else 0 leader_name = p.leader.name if p.leader else None # 里程碑数据 ms_rows = db.query(ProjectMilestone).filter( ProjectMilestone.project_id == p.id ).order_by(ProjectMilestone.phase, ProjectMilestone.sort_order).all() milestones_out = [ MilestoneOut( id=m.id, name=m.name, phase=m.phase.value if hasattr(m.phase, 'value') else m.phase, is_completed=bool(m.is_completed), completed_at=m.completed_at, sort_order=m.sort_order, ) for m in ms_rows ] # 阶段摘要 pre_ms = [m for m in ms_rows if (m.phase.value if hasattr(m.phase, 'value') else m.phase) == "前期"] post_ms = [m for m in ms_rows if (m.phase.value if hasattr(m.phase, 'value') else m.phase) == "后期"] pre_completed = sum(1 for m in pre_ms if m.is_completed) post_completed = sum(1 for m in post_ms if m.is_completed) phase_summary = { "pre": {"total": len(pre_ms), "completed": pre_completed}, "production": { "progress_percent": progress, "submitted_seconds": round(total_secs, 1), "target_seconds": target, }, "post": {"total": len(post_ms), "completed": post_completed}, } # 自动推断当前阶段 if len(pre_ms) > 0 and pre_completed < len(pre_ms): current_stage = "前期" elif progress < 100: current_stage = "制作" elif len(post_ms) > 0 and post_completed < len(post_ms): current_stage = "后期" else: current_stage = "已完成" return ProjectOut( id=p.id, name=p.name, project_type=p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, status=p.status.value if hasattr(p.status, 'value') else p.status, leader_id=p.leader_id, leader_name=leader_name, current_phase=p.current_phase.value if hasattr(p.current_phase, 'value') else p.current_phase, episode_duration_minutes=p.episode_duration_minutes, episode_count=p.episode_count, target_total_seconds=target, estimated_completion_date=p.estimated_completion_date, actual_completion_date=p.actual_completion_date, contract_amount=p.contract_amount, created_at=p.created_at, total_submitted_seconds=round(total_secs, 1), progress_percent=progress, waste_seconds=round(waste, 1), waste_rate=waste_rate, milestones=milestones_out, phase_summary=phase_summary, current_stage=current_stage, ) @router.get("/", response_model=List[ProjectOut]) def list_projects( status: Optional[str] = Query(None), project_type: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): q = db.query(Project) if status: q = q.filter(Project.status == ProjectStatus(status)) if project_type: q = q.filter(Project.project_type == ProjectType(project_type)) projects = q.order_by(Project.created_at.desc()).all() return [enrich_project(p, db) for p in projects] @router.post("/", response_model=ProjectOut) def create_project( req: ProjectCreate, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:create")) ): project = Project( name=req.name, project_type=ProjectType(req.project_type), leader_id=req.leader_id, current_phase=PhaseGroup(req.current_phase), episode_duration_minutes=req.episode_duration_minutes, episode_count=req.episode_count, estimated_completion_date=req.estimated_completion_date, contract_amount=req.contract_amount, ) db.add(project) db.flush() # 创建里程碑 ms_list = req.milestones if req.milestones else DEFAULT_MILESTONES for ms in ms_list: db.add(ProjectMilestone( project_id=project.id, name=ms["name"], phase=PhaseGroup(ms["phase"]), sort_order=ms.get("sort_order", 0), )) db.commit() db.refresh(project) return enrich_project(project, db) @router.get("/{project_id}", response_model=ProjectOut) def get_project( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): p = db.query(Project).filter(Project.id == project_id).first() if not p: raise HTTPException(status_code=404, detail="项目不存在") return enrich_project(p, db) @router.put("/{project_id}", response_model=ProjectOut) def update_project( project_id: int, req: ProjectUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:edit")) ): p = db.query(Project).filter(Project.id == project_id).first() if not p: raise HTTPException(status_code=404, detail="项目不存在") if req.name is not None: p.name = req.name if req.project_type is not None: p.project_type = ProjectType(req.project_type) if req.status is not None: new_status = ProjectStatus(req.status) p.status = new_status if new_status in (ProjectStatus.IN_PROGRESS, ProjectStatus.ABANDONED): p.actual_completion_date = None if req.leader_id is not None: p.leader_id = req.leader_id if req.current_phase is not None: p.current_phase = PhaseGroup(req.current_phase) if req.episode_duration_minutes is not None: p.episode_duration_minutes = req.episode_duration_minutes if req.episode_count is not None: p.episode_count = req.episode_count if req.estimated_completion_date is not None: p.estimated_completion_date = req.estimated_completion_date if req.actual_completion_date is not None: p.actual_completion_date = req.actual_completion_date if req.contract_amount is not None: p.contract_amount = req.contract_amount db.commit() db.refresh(p) return enrich_project(p, db) @router.delete("/{project_id}") def delete_project( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:delete")) ): """删除项目及其关联数据""" from models import OutsourceCost, AIToolCostAllocation, CostOverride, SubmissionHistory p = db.query(Project).filter(Project.id == project_id).first() if not p: raise HTTPException(status_code=404, detail="项目不存在") # 删除关联数据 subs = db.query(Submission).filter(Submission.project_id == project_id).all() for s in subs: db.query(SubmissionHistory).filter(SubmissionHistory.submission_id == s.id).delete() db.query(Submission).filter(Submission.project_id == project_id).delete() db.query(OutsourceCost).filter(OutsourceCost.project_id == project_id).delete() db.query(AIToolCostAllocation).filter(AIToolCostAllocation.project_id == project_id).delete() db.query(CostOverride).filter(CostOverride.project_id == project_id).delete() db.query(ProjectMilestone).filter(ProjectMilestone.project_id == project_id).delete() db.delete(p) db.commit() return {"message": "项目已删除"} @router.post("/{project_id}/complete") def complete_project( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:complete")) ): """Owner 手动确认项目完成""" p = db.query(Project).filter(Project.id == project_id).first() if not p: raise HTTPException(status_code=404, detail="项目不存在") from datetime import date as date_today p.status = ProjectStatus.COMPLETED p.actual_completion_date = date_today.today() db.commit() return {"message": "项目已标记为完成", "project_id": project_id} # ──────────────────── 里程碑管理 ──────────────────── @router.get("/{project_id}/milestones", response_model=List[MilestoneOut]) def list_milestones( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): ms = db.query(ProjectMilestone).filter( ProjectMilestone.project_id == project_id ).order_by(ProjectMilestone.phase, ProjectMilestone.sort_order).all() return [ MilestoneOut( id=m.id, name=m.name, phase=m.phase.value if hasattr(m.phase, 'value') else m.phase, is_completed=bool(m.is_completed), completed_at=m.completed_at, sort_order=m.sort_order, ) for m in ms ] @router.post("/{project_id}/milestones", response_model=MilestoneOut) def add_milestone( project_id: int, req: MilestoneCreate, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:edit")) ): p = db.query(Project).filter(Project.id == project_id).first() if not p: raise HTTPException(status_code=404, detail="项目不存在") # 找到同阶段最大 sort_order max_order = db.query(sa_func.max(ProjectMilestone.sort_order)).filter( ProjectMilestone.project_id == project_id, ProjectMilestone.phase == PhaseGroup(req.phase), ).scalar() or 0 m = ProjectMilestone( project_id=project_id, name=req.name, phase=PhaseGroup(req.phase), sort_order=max_order + 1, ) db.add(m) db.commit() db.refresh(m) return MilestoneOut( id=m.id, name=m.name, phase=m.phase.value, is_completed=False, completed_at=None, sort_order=m.sort_order, ) @router.put("/milestones/{milestone_id}/toggle") def toggle_milestone( milestone_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:edit")) ): m = db.query(ProjectMilestone).filter(ProjectMilestone.id == milestone_id).first() if not m: raise HTTPException(status_code=404, detail="里程碑不存在") m.is_completed = 0 if m.is_completed else 1 m.completed_at = datetime.now() if m.is_completed else None db.commit() return {"id": m.id, "is_completed": bool(m.is_completed)} @router.delete("/milestones/{milestone_id}") def delete_milestone( milestone_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("project:edit")) ): m = db.query(ProjectMilestone).filter(ProjectMilestone.id == milestone_id).first() if not m: raise HTTPException(status_code=404, detail="里程碑不存在") db.delete(m) db.commit() return {"message": "已删除"}