168 lines
5.1 KiB
Python
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),
|
|
}
|
|
|