Files
ai-ticket-platform/backend/apps/workitems/api.py
2026-07-27 19:00:35 +08:00

102 lines
3.4 KiB
Python

from datetime import timedelta
from django.db.models import Count, F
from django.utils import timezone
from ninja import Router
from ninja.errors import HttpError
from apps.accounts.api import require_permission
from apps.workitems.models import WorkItem
from apps.workitems.schemas import WorkItemMetricsOut, WorkItemOut
router = Router(tags=["tasks"])
def serialize_work_item(item: WorkItem) -> dict:
now = timezone.now()
if item.status != "active":
completed_overdue = (
item.due_at and item.completed_at and item.completed_at > item.due_at
)
sla_status = "completed_overdue" if completed_overdue else "completed"
elif item.due_at and item.due_at <= now:
sla_status = "overdue"
elif item.due_at and item.due_at <= now + timedelta(hours=2):
sla_status = "due_soon"
else:
sla_status = "on_track"
return {
"public_id": str(item.public_id),
"task_type": item.task_type,
"status": item.status,
"object_type": item.object_type,
"object_id": item.object_id,
"ticket_id": item.ticket_id,
"ticket_no": item.ticket.ticket_no if item.ticket_id else None,
"title": item.title,
"description": item.description,
"payload": item.payload,
"assignee_id": item.assignee_id,
"created_at": item.created_at,
"updated_at": item.updated_at,
"completed_at": item.completed_at,
"due_at": item.due_at,
"sla_status": sla_status,
}
@router.get("/metrics", response=WorkItemMetricsOut)
def get_work_item_metrics(request):
require_permission(request, "ticket.view")
now = timezone.now()
active = WorkItem.objects.filter(status="active")
completed = WorkItem.objects.filter(status="completed", completed_at__isnull=False)
by_task_type = {
item["task_type"]: item["total"]
for item in active.values("task_type").annotate(total=Count("id"))
}
return {
"active_count": active.count(),
"overdue_count": active.filter(due_at__lte=now).count(),
"due_soon_count": active.filter(
due_at__gt=now,
due_at__lte=now + timedelta(hours=2),
).count(),
"completed_within_sla_count": completed.filter(
completed_at__lte=F("due_at")
).count(),
"completed_overdue_count": completed.filter(
completed_at__gt=F("due_at")
).count(),
"by_task_type": by_task_type,
}
@router.get("", response=list[WorkItemOut])
def list_work_items(
request,
status: str | None = "active",
task_type: str | None = None,
ticket_id: int | None = None,
):
require_permission(request, "ticket.view")
queryset = WorkItem.objects.select_related("ticket", "assignee")
if status:
queryset = queryset.filter(status=status)
if task_type:
queryset = queryset.filter(task_type=task_type)
if ticket_id:
queryset = queryset.filter(ticket_id=ticket_id)
return [serialize_work_item(item) for item in queryset[:200]]
@router.get("/{public_id}", response=WorkItemOut)
def get_work_item(request, public_id: str):
require_permission(request, "ticket.view")
item = WorkItem.objects.select_related("ticket", "assignee").filter(
public_id=public_id
).first()
if item is None:
raise HttpError(404, "任务不存在")
return serialize_work_item(item)