from __future__ import annotations from .shared import * from ...services.context_cache_service import context_cache_service from ...services.sstc.entity_lookup import _ensure_project, _ensure_task from ...services.sstc.governance import _is_manager from ...services.sstc.signal_core import _parse_node_id, _parse_project_id, _sync_pmflow_rag_async router = APIRouter(tags=["sstc"]) @router.get("/projects/{project_id}/tasks/board") async def get_task_board( project_id: str, assignee_id: Optional[int] = Query(default=None), priority: Optional[str] = Query(default=None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): pid = _parse_project_id(project_id) tenant_id = int(current_user.tenant_id) await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid) result = await db.execute( select(Task).where( and_( Task.project_id == pid, Task.tenant_id == tenant_id, ) ) ) tasks = list(result.scalars().all()) if assignee_id is not None: tasks = [t for t in tasks if t.assignee_id == assignee_id] if priority: tasks = [t for t in tasks if (t.priority or "").lower() == priority.lower()] rows = [] for t in tasks: risk_count = 0 if (t.status or "").lower() == "blocked": risk_count += 1 if t.due_date and t.due_date < now_cn_aware().replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES: risk_count += 1 rows.append( { "id": str(t.id), "title": t.title, "description": t.description, "status": t.status, "priority": t.priority, "assignee_id": str(t.assignee_id) if t.assignee_id else None, "due_date": t.due_date.isoformat() if t.due_date else None, "estimated_hours": float(t.estimated_hours) if t.estimated_hours else None, "risk_count": risk_count, "truth_node_id": str(t.truth_node_id), "required_skill_tag_ids": list(t.required_skill_tag_ids or []), "ai_recommended_assignees": t.ai_recommended_assignees, "ai_breakdown_payload": t.ai_breakdown_payload, "manual_overrides": t.manual_overrides, "auto_assigned": bool(t.assignee_id) and not bool((t.manual_overrides or {}).get("assignee_id")), } ) return success_response(rows) @router.patch("/projects/{project_id}/tasks/{task_id}/status") async def patch_task_status( project_id: str, task_id: str, body: TaskStatusPatchRequest, background_tasks: BackgroundTasks, permission_user: User = Depends(require_project_member_or_manager), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): _ = permission_user pid = _parse_project_id(project_id) tid = _parse_node_id(task_id, "task_id") tenant_id = int(current_user.tenant_id) await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid) task = await _ensure_task(db, tenant_id=tenant_id, project_id=pid, task_id=tid) if not _is_manager(current_user) and int(task.assignee_id or 0) != int(current_user.id): raise HTTPException(status_code=403, detail="仅负责人或任务执行人可更新状态") before_status = (task.status or "").lower() before = {"status": task.status} task.status = body.status if body.status in DONE_STATUSES: task.completed_at = now_cn_naive() await log_operation( db, tenant_id=tenant_id, user_id=int(current_user.id), project_id=pid, entity_type="task", entity_id=str(tid), action="status_change", before_data=before, after_data={"status": body.status}, ) if task.assignee_id: await workload_service.refresh_user_assigned_hours_from_tasks( db, tenant_id=tenant_id, user_id=int(task.assignee_id), ) else: await db.commit() await context_cache_service.invalidate_system_context(tenant_id=tenant_id) if task.assignee_id: await context_cache_service.invalidate_user_context( tenant_id=tenant_id, user_id=int(task.assignee_id), ) became_done = (body.status or "").lower() in DONE_STATUSES and before_status not in DONE_STATUSES if became_done and task.assignee_id: background_tasks.add_task( skill_score_service.process_task_completion, task_id=int(task.id), tenant_id=tenant_id, operator_user_id=int(current_user.id), ) _sync_pmflow_rag_async( tenant_id=tenant_id, record_type="task", record_id=int(task.id), project_id=pid, title=task.title, content=(task.description or "") + f"\n状态: {task.status}", ) return success_response({"id": str(task.id), "status": task.status})