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