199 lines
6.0 KiB
Python
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),
|
|
}
|