PMFlow/backend/app/services/context_cache_service/mixin_project_team.py

208 lines
7.5 KiB
Python

from __future__ import annotations
from .shared import *
class ContextCacheProjectTeamMixin:
async def _get_project_list_cached(self, *, db: AsyncSession, tenant_id: int) -> dict[str, Any]:
key = self._project_list_key(tenant_id)
cached = await self._get_json(key)
if cached is not None:
return cached
project_rows = (
await db.execute(
select(
Project.id,
Project.name,
Project.title,
Project.status,
Project.owner_id,
User.name.label("owner_name"),
)
.outerjoin(
User,
and_(
User.tenant_id == tenant_id,
User.id == Project.owner_id,
),
)
.where(Project.tenant_id == tenant_id)
.order_by(Project.updated_at.desc(), Project.id.desc())
)
).all()
project_ids = [int(row.id) for row in project_rows]
signal_count_map: dict[int, int] = {}
if project_ids:
signal_rows = (
await db.execute(
select(ProjectSignalLink.project_id, func.count(ProjectSignalLink.id))
.where(
and_(
ProjectSignalLink.tenant_id == tenant_id,
ProjectSignalLink.project_id.in_(project_ids),
)
)
.group_by(ProjectSignalLink.project_id)
)
).all()
signal_count_map = {int(pid): int(cnt or 0) for pid, cnt in signal_rows}
task_count_map: dict[int, int] = {}
task_done_map: dict[int, int] = {}
if project_ids:
done_case = case((func.lower(Task.status).in_(DONE_TASK_STATUSES), 1), else_=0)
task_rows = (
await db.execute(
select(
Task.project_id,
func.count(Task.id).label("task_total"),
func.sum(done_case).label("done_count"),
)
.where(
and_(
Task.tenant_id == tenant_id,
Task.project_id.in_(project_ids),
)
)
.group_by(Task.project_id)
)
).all()
task_count_map = {int(pid): int(total or 0) for pid, total, _ in task_rows}
task_done_map = {int(pid): int(done or 0) for pid, _, done in task_rows}
items: list[dict[str, Any]] = []
for row in project_rows:
pid = int(row.id)
task_total = int(task_count_map.get(pid, 0))
done_total = int(task_done_map.get(pid, 0))
completion_rate = round((done_total / task_total) * 100, 2) if task_total > 0 else 0.0
title = str(row.title or row.name or f"Project#{pid}")
items.append(
{
"id": pid,
"name": title,
"status": str(row.status or "unknown"),
"current_stage": str(row.status or "unknown"),
"owner_id": _safe_int(row.owner_id, 0) or None,
"owner_name": str(row.owner_name or "未知"),
"signal_count": int(signal_count_map.get(pid, 0)),
"task_count": task_total,
"completion_rate": completion_rate,
}
)
payload = {
"items": items,
"count": len(items),
"snapshot_at": now_cn_aware().isoformat(),
}
await self._set_json(key, payload, PROJECT_LIST_TTL_SECONDS)
return payload
async def _build_team_members_snapshot(self, *, db: AsyncSession, tenant_id: int) -> list[dict[str, Any]]:
users = (
await db.execute(
select(User.id, User.name, User.role, User.workload).where(
and_(
User.tenant_id == tenant_id,
User.is_active.is_(True),
)
)
)
).all()
if not users:
return []
user_ids = [int(u.id) for u in users]
workload_rows = (
await db.execute(
select(UserWorkload.user_id, UserWorkload.workload_ratio).where(
and_(
UserWorkload.tenant_id == tenant_id,
UserWorkload.user_id.in_(user_ids),
)
)
)
).all()
workload_map = {int(uid): float(ratio or 0.0) for uid, ratio in workload_rows}
project_rows = (
await db.execute(
select(UserProject.user_id, UserProject.project_id).where(
and_(
UserProject.tenant_id == tenant_id,
UserProject.user_id.in_(user_ids),
)
)
)
).all()
projects_map: dict[int, list[int]] = {}
for uid, pid in project_rows:
projects_map.setdefault(int(uid), []).append(int(pid))
result: list[dict[str, Any]] = []
for row in users:
uid = int(row.id)
ratio = workload_map.get(uid)
if ratio is None:
ratio = round(float(row.workload or 0) / 100.0, 2)
result.append(
{
"id": uid,
"name": str(row.name or f"User#{uid}"),
"role": str(row.role or "unknown"),
"workload_ratio": float(ratio),
"assigned_projects": projects_map.get(uid, []),
}
)
return result
async def _get_pending_signal_cached(self, *, db: AsyncSession, tenant_id: int) -> dict[str, Any]:
key = self._signal_pending_key(tenant_id)
cached = await self._get_json(key)
if cached is not None:
return cached
pending_filter = or_(
Signal.processed.is_(False),
func.lower(Signal.status).in_(PENDING_SIGNAL_STATUSES),
)
p0_case = case(
(
func.lower(Signal.priority).in_({"critical", "p0", "urgent", "highest"}),
1,
),
else_=0,
)
p1_case = case((func.lower(Signal.priority).in_({"high", "p1"}), 1), else_=0)
p2_case = case((func.lower(Signal.priority).in_({"medium", "p2", "normal"}), 1), else_=0)
row = (
await db.execute(
select(
func.count(Signal.id),
func.sum(p0_case),
func.sum(p1_case),
func.sum(p2_case),
).where(
and_(
Signal.tenant_id == tenant_id,
pending_filter,
)
)
)
).first()
total, p0_count, p1_count, p2_count = row or (0, 0, 0, 0)
payload = {
"total": int(total or 0),
"p0_count": int(p0_count or 0),
"p1_count": int(p1_count or 0),
"p2_count": int(p2_count or 0),
"snapshot_at": now_cn_aware().isoformat(),
}
await self._set_json(key, payload, SIGNAL_PENDING_TTL_SECONDS)
return payload