PMFlow/backend/app/services/skill/workload_recommend.py

168 lines
5.1 KiB
Python

from __future__ import annotations
from .shared import *
async def update_user_workload(
db: AsyncSession,
tenant_id: int,
user_id: int,
total_tasks_delta: int = 0,
completed_tasks_delta: int = 0,
in_progress_delta: int = 0,
estimated_hours_delta: float = 0,
completed_hours_delta: float = 0,
) -> Optional[UserWorkload]:
"""
更新用户工作量。
新 schema: current_assigned_hours, workload_ratio
"""
result = await db.execute(
select(UserWorkload).where(
and_(
UserWorkload.tenant_id == tenant_id,
UserWorkload.user_id == user_id,
)
)
)
workload = result.scalar_one_or_none()
if not workload:
workload = UserWorkload(
tenant_id=tenant_id,
user_id=user_id,
current_assigned_hours=0.0,
max_available_hours_per_week=40.0,
workload_ratio=0.0,
)
db.add(workload)
await db.flush()
delta = estimated_hours_delta + completed_hours_delta
workload.current_assigned_hours = max(
0, float(workload.current_assigned_hours or 0) + delta
)
max_hours = float(workload.max_available_hours_per_week or 40)
workload.workload_ratio = min(
100.0, (workload.current_assigned_hours / max_hours) * 100
)
await db.commit()
await db.refresh(workload)
user_result = await db.execute(select(User).where(User.id == user_id))
user = user_result.scalar_one_or_none()
if user:
user.workload = int(workload.workload_ratio)
await db.commit()
return workload
async def recommend_personnel(
db: AsyncSession,
tenant_id: int,
required_skills: List[str],
task_description: Optional[str] = None,
task_id: Optional[str] = None,
limit: int = 5,
) -> dict:
"""
根据所需技能推荐匹配人员。
返回匹配度评分、推荐理由等。
"""
if not required_skills:
return {
"task_id": task_id,
"task_description": task_description,
"required_skills": [],
"recommendations": [],
"total_count": 0,
}
# 通过 slug 或 name 匹配技能标签
skill_tags_result = await db.execute(
select(SkillTag).where(
and_(
SkillTag.tenant_id == tenant_id,
(SkillTag.slug.in_(required_skills) | SkillTag.name.in_(required_skills)),
)
)
)
tags = skill_tags_result.scalars().all()
tag_ids = [t.id for t in tags]
if not tag_ids:
return {
"task_id": task_id,
"task_description": task_description,
"required_skills": required_skills,
"recommendations": [],
"total_count": 0,
}
# 查询拥有这些技能的用户及熟练度
bindings_result = await db.execute(
select(UserSkillBinding, SkillTag, User)
.join(SkillTag, UserSkillBinding.skill_tag_id == SkillTag.id)
.join(User, UserSkillBinding.user_id == User.id)
.where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.skill_tag_id.in_(tag_ids),
User.status == UserStatus.ACTIVE,
)
)
)
rows = bindings_result.all()
# 按用户聚合
user_scores: dict = {}
for b, st, u in rows:
uid = str(u.id)
if uid not in user_scores:
user_scores[uid] = {
"user": u,
"skills": [],
"proficiencies": [],
"match_count": 0,
}
user_scores[uid]["skills"].append(st.name)
user_scores[uid]["proficiencies"].append(b.proficiency)
user_scores[uid]["match_count"] = len(user_scores[uid]["skills"])
# 计算匹配度并排序
recommendations = []
for uid, data in user_scores.items():
u = data["user"]
avg_prof = (
sum(data["proficiencies"]) / len(data["proficiencies"])
if data["proficiencies"]
else 0
)
match_ratio = len(data["skills"]) / len(required_skills) if required_skills else 0
match_score = min(100, (match_ratio * 60 + avg_prof * 0.4))
reason = f"匹配技能: {', '.join(data['skills'])}"
recommendations.append(
{
"user_id": uid,
"user_name": u.name,
"match_score": round(match_score, 1),
"reason": reason,
"skill_match_count": data["match_count"],
"avg_proficiency": round(avg_prof, 1),
"workload_rate": float(u.workload) if u.workload else 0,
"recommended_role": u.role.value if hasattr(u.role, "value") else str(u.role),
}
)
recommendations.sort(key=lambda x: (-x["match_score"], -x["skill_match_count"]))
recommendations = recommendations[:limit]
return {
"task_id": task_id,
"task_description": task_description,
"required_skills": required_skills,
"recommendations": recommendations,
"total_count": len(recommendations),
}