PMFlow/backend/app/api/projects_api/helpers.py

266 lines
7.4 KiB
Python

from __future__ import annotations
from .shared import *
def _as_dict(value: object) -> dict[str, Any]:
if isinstance(value, dict):
return value
if isinstance(value, str) and value.strip():
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, dict) else {"text": value}
except (TypeError, ValueError):
return {"text": value}
return {}
def _serialize_project_row(project: Project) -> dict:
return {
"id": str(project.id),
"title": project.title or project.name,
"description": project.description,
"status": project.status,
"domain_id": str(project.domain_id) if project.domain_id is not None else None,
"assigned_role": project.assigned_role,
"signal_count": int(project.signal_count or 0),
"created_at": project.created_at.isoformat() if project.created_at else None,
"updated_at": project.updated_at.isoformat() if project.updated_at else None,
}
async def _build_project_summary(
db: AsyncSession,
*,
tenant_id: int,
project: Project,
) -> dict:
base = _serialize_project_row(project)
signal_count_result = await db.execute(
select(func.count(ProjectSignalLink.id)).where(
and_(
ProjectSignalLink.tenant_id == tenant_id,
ProjectSignalLink.project_id == int(project.id),
)
)
)
signal_count = int(signal_count_result.scalar() or 0)
base["signal_count"] = signal_count
task_rows = (
await db.execute(
select(Task).where(
and_(
Task.tenant_id == tenant_id,
Task.project_id == int(project.id),
)
)
)
).scalars().all()
total_tasks = len(task_rows)
completed_tasks = 0
in_progress_tasks = 0
pending_tasks = 0
task_items: list[dict] = []
for task in task_rows:
status_value = (task.status or "").lower()
if status_value in DONE_STATUSES:
completed_tasks += 1
elif status_value in {"in_progress", "review", "blocked"}:
in_progress_tasks += 1
else:
pending_tasks += 1
task_items.append(
{
"task_name": task.title,
"type": "task",
"status": task.status,
"signal_id": str(task.truth_node_id) if task.truth_node_id is not None else "",
}
)
base["progress"] = round((completed_tasks / total_tasks) * 100, 2) if total_tasks else 0.0
base["total_tasks"] = total_tasks
base["completed_tasks"] = completed_tasks
base["in_progress_tasks"] = in_progress_tasks
base["pending_tasks"] = pending_tasks
base["tasks"] = task_items
return base
async def _fetch_project_member_user_ids(
db: AsyncSession,
*,
tenant_id: int,
project_id: int,
) -> list[int]:
rows = await db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == int(tenant_id),
UserProject.project_id == int(project_id),
)
)
)
return sorted(int(user_id) for user_id in rows.scalars().all() if user_id is not None)
async def _get_project_or_404(
db: AsyncSession,
*,
tenant_id: int,
project_id: int,
) -> Project:
project = (
await db.execute(
select(Project).where(
and_(
Project.tenant_id == tenant_id,
Project.id == project_id,
)
)
)
).scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
async def _cleanup_project_dependencies(
db: AsyncSession,
*,
tenant_id: int,
project_id: int,
) -> None:
task_ids_subquery = (
select(Task.id)
.where(
and_(
Task.tenant_id == tenant_id,
Task.project_id == project_id,
)
)
.scalar_subquery()
)
# 先断开任务子依赖,避免删除 tasks 时触发约束错误。
await db.execute(
update(CostTracking)
.where(
and_(
CostTracking.tenant_id == tenant_id,
CostTracking.task_id.in_(task_ids_subquery),
)
)
.values(task_id=None)
)
# 强关联依赖:删除项目时同步清理关联运行/执行记录。
await db.execute(
delete(AIWorkerExecution).where(
and_(
AIWorkerExecution.tenant_id == tenant_id,
AIWorkerExecution.project_id == project_id,
)
)
)
await db.execute(
delete(AIOrchestrationRun).where(
and_(
AIOrchestrationRun.tenant_id == tenant_id,
AIOrchestrationRun.project_id == project_id,
)
)
)
await db.execute(
delete(SignalSourceRegistry).where(
and_(
SignalSourceRegistry.tenant_id == tenant_id,
SignalSourceRegistry.project_id == project_id,
)
)
)
await db.execute(
delete(ProjectSignalLink).where(
and_(
ProjectSignalLink.tenant_id == tenant_id,
ProjectSignalLink.project_id == project_id,
)
)
)
await db.execute(
delete(Task).where(
and_(
Task.tenant_id == tenant_id,
Task.project_id == project_id,
)
)
)
await db.execute(
delete(UserProject).where(
and_(
UserProject.tenant_id == tenant_id,
UserProject.project_id == project_id,
)
)
)
# 可空外键:保留记录,将 project 解绑为 null。
await db.execute(
update(WorkItem)
.where(
and_(
WorkItem.tenant_id == tenant_id,
WorkItem.project_id == project_id,
)
)
.values(project_id=None)
)
await db.execute(
update(OperationLog)
.where(
and_(
OperationLog.tenant_id == tenant_id,
OperationLog.project_id == project_id,
)
)
.values(project_id=None)
)
await db.execute(
update(OperationExecutionLedger)
.where(
and_(
OperationExecutionLedger.tenant_id == tenant_id,
OperationExecutionLedger.project_id == project_id,
)
)
.values(project_id=None)
)
await db.execute(
update(Experiment)
.where(
and_(
Experiment.tenant_id == tenant_id,
Experiment.project_id == project_id,
)
)
.values(project_id=None)
)
await db.execute(
update(Budget)
.where(
and_(
Budget.tenant_id == tenant_id,
Budget.project_id == project_id,
)
)
.values(project_id=None)
)
await db.execute(
update(ConversationSession)
.where(
and_(
ConversationSession.tenant_id == tenant_id,
ConversationSession.current_project_id == project_id,
)
)
.values(current_project_id=None)
)