208 lines
7.5 KiB
Python
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
|
|
|