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

199 lines
6.0 KiB
Python

from __future__ import annotations
from sqlalchemy import or_
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 | int],
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,
}
skill_ids: list[int] = []
skill_names: list[str] = []
for raw in required_skills:
if isinstance(raw, bool):
continue
try:
skill_id = int(str(raw).strip())
except (TypeError, ValueError):
skill_id = 0
if skill_id > 0 and str(raw).strip() == str(skill_id):
skill_ids.append(skill_id)
continue
text = str(raw or "").strip()
if text:
skill_names.append(text)
conditions = []
if skill_ids:
conditions.append(SkillTag.id.in_(skill_ids))
if skill_names:
conditions.append(or_(SkillTag.slug.in_(skill_names), SkillTag.name.in_(skill_names)))
if not conditions:
return {
"task_id": task_id,
"task_description": task_description,
"required_skills": list(required_skills),
"recommendations": [],
"total_count": 0,
}
skill_tags_result = await db.execute(
select(SkillTag).where(
and_(
SkillTag.tenant_id == tenant_id,
or_(*conditions),
)
)
)
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),
}