197 lines
5.6 KiB
Python
197 lines
5.6 KiB
Python
"""Data loading helpers for signal planner service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .shared import (
|
|
Any,
|
|
AsyncSession,
|
|
Optional,
|
|
Project,
|
|
Signal,
|
|
SkillTag,
|
|
User,
|
|
UserProject,
|
|
UserSkillBinding,
|
|
UserWorkload,
|
|
and_,
|
|
select,
|
|
)
|
|
|
|
|
|
class SignalPlannerLoadersMixin:
|
|
"""Load signal/project/member contexts for planner prompt."""
|
|
|
|
@classmethod
|
|
async def _load_signal(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
signal_id: int,
|
|
) -> Signal | None:
|
|
return (
|
|
await db.execute(
|
|
select(Signal)
|
|
.where(
|
|
and_(
|
|
Signal.tenant_id == int(tenant_id),
|
|
Signal.id == int(signal_id),
|
|
)
|
|
)
|
|
.limit(1)
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
@classmethod
|
|
async def _load_project_title(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
project_id: Optional[int],
|
|
) -> str:
|
|
if project_id is None:
|
|
return ""
|
|
row = (
|
|
await db.execute(
|
|
select(Project.title)
|
|
.where(
|
|
and_(
|
|
Project.tenant_id == int(tenant_id),
|
|
Project.id == int(project_id),
|
|
)
|
|
)
|
|
.limit(1)
|
|
)
|
|
).scalar_one_or_none()
|
|
return str(row or "")
|
|
|
|
@classmethod
|
|
async def _load_recent_signal_summaries(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
project_id: Optional[int],
|
|
exclude_signal_id: int,
|
|
) -> list[dict[str, Any]]:
|
|
if project_id is None:
|
|
return []
|
|
rows = (
|
|
await db.execute(
|
|
select(Signal.id, Signal.title, Signal.summary, Signal.priority)
|
|
.where(
|
|
and_(
|
|
Signal.tenant_id == int(tenant_id),
|
|
Signal.matched_project_id == int(project_id),
|
|
Signal.id != int(exclude_signal_id),
|
|
)
|
|
)
|
|
.order_by(Signal.created_at.desc())
|
|
.limit(8)
|
|
)
|
|
).all()
|
|
items: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
items.append(
|
|
{
|
|
"id": int(row[0]),
|
|
"title": str(row[1] or "")[:120],
|
|
"summary": str(row[2] or "")[:180],
|
|
"priority": str(row[3] or "")[:20],
|
|
}
|
|
)
|
|
return items
|
|
|
|
@classmethod
|
|
async def _load_member_profiles(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
project_id: Optional[int],
|
|
) -> list[dict[str, Any]]:
|
|
if project_id is None:
|
|
return []
|
|
|
|
rows = (
|
|
await db.execute(
|
|
select(
|
|
User.id,
|
|
User.name,
|
|
User.role,
|
|
UserWorkload.workload_ratio,
|
|
SkillTag.name,
|
|
UserSkillBinding.proficiency,
|
|
)
|
|
.join(
|
|
UserProject,
|
|
and_(
|
|
UserProject.tenant_id == int(tenant_id),
|
|
UserProject.user_id == User.id,
|
|
UserProject.project_id == int(project_id),
|
|
),
|
|
)
|
|
.join(
|
|
UserWorkload,
|
|
and_(
|
|
UserWorkload.tenant_id == int(tenant_id),
|
|
UserWorkload.user_id == User.id,
|
|
),
|
|
isouter=True,
|
|
)
|
|
.join(
|
|
UserSkillBinding,
|
|
and_(
|
|
UserSkillBinding.tenant_id == int(tenant_id),
|
|
UserSkillBinding.user_id == User.id,
|
|
),
|
|
isouter=True,
|
|
)
|
|
.join(
|
|
SkillTag,
|
|
and_(
|
|
SkillTag.tenant_id == int(tenant_id),
|
|
SkillTag.id == UserSkillBinding.skill_tag_id,
|
|
),
|
|
isouter=True,
|
|
)
|
|
.where(
|
|
and_(
|
|
User.tenant_id == int(tenant_id),
|
|
User.is_active == True,
|
|
)
|
|
)
|
|
.order_by(User.id.asc())
|
|
)
|
|
).all()
|
|
|
|
by_user: dict[int, dict[str, Any]] = {}
|
|
for row in rows:
|
|
user_id = cls._safe_int(row[0])
|
|
if user_id is None:
|
|
continue
|
|
profile = by_user.setdefault(
|
|
int(user_id),
|
|
{
|
|
"user_id": int(user_id),
|
|
"name": str(row[1] or "成员"),
|
|
"role": str(row[2] or ""),
|
|
"workload_ratio": float(row[3] or 0.0),
|
|
"skills": [],
|
|
},
|
|
)
|
|
skill_name = str(row[4] or "").strip()
|
|
proficiency = cls._safe_int(row[5])
|
|
if skill_name:
|
|
profile["skills"].append(
|
|
{
|
|
"name": skill_name,
|
|
"proficiency": proficiency,
|
|
}
|
|
)
|
|
items = list(by_user.values())
|
|
for item in items:
|
|
item["skills"] = item["skills"][:10]
|
|
return items
|