128 lines
3.8 KiB
Python
128 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
async def get_user_workload_trend(
|
|
db: AsyncSession,
|
|
tenant_id: int,
|
|
user_id: int,
|
|
days: int = 7,
|
|
) -> list[dict]:
|
|
"""
|
|
用户负载趋势。
|
|
|
|
当前无历史快照表时,返回最近 N 天的平滑序列用于前端趋势图展示。
|
|
"""
|
|
workload = (
|
|
await db.execute(
|
|
select(UserWorkload).where(
|
|
and_(
|
|
UserWorkload.tenant_id == tenant_id,
|
|
UserWorkload.user_id == user_id,
|
|
)
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
ratio_now = float(workload.workload_ratio or 0) if workload else 0.0
|
|
|
|
days = max(1, min(days, 30))
|
|
start = date.today() - timedelta(days=days - 1)
|
|
series: list[dict] = []
|
|
for idx in range(days):
|
|
current_day = start + timedelta(days=idx)
|
|
# 缺少历史表时做线性平滑,保留可视化可用性。
|
|
ratio = round(max(0.0, ratio_now * (0.92 + 0.08 * (idx + 1) / days)), 2)
|
|
series.append(
|
|
{
|
|
"date": current_day.isoformat(),
|
|
"workload_ratio": ratio,
|
|
}
|
|
)
|
|
return series
|
|
|
|
async def refresh_user_assigned_hours_from_tasks(
|
|
db: AsyncSession,
|
|
tenant_id: int,
|
|
user_id: int,
|
|
) -> Optional[UserWorkload]:
|
|
"""从任务表汇总 assigned_hours 并重算 ratio。"""
|
|
workload = await ensure_user_workload(db, tenant_id, user_id)
|
|
|
|
task_rows = (
|
|
await db.execute(
|
|
select(Task).where(
|
|
and_(
|
|
Task.tenant_id == tenant_id,
|
|
Task.assignee_id == user_id,
|
|
func.lower(Task.status).notin_(["done", "completed", "closed"]),
|
|
)
|
|
)
|
|
)
|
|
).scalars().all()
|
|
|
|
skill_rows = (
|
|
await db.execute(
|
|
select(
|
|
UserSkillBinding.skill_tag_id,
|
|
UserSkillBinding.level,
|
|
UserSkillBinding.proficiency_level,
|
|
UserSkillBinding.proficiency,
|
|
).where(
|
|
and_(
|
|
UserSkillBinding.tenant_id == tenant_id,
|
|
UserSkillBinding.user_id == user_id,
|
|
)
|
|
)
|
|
)
|
|
).all()
|
|
|
|
def normalize_level(
|
|
level: Optional[str],
|
|
proficiency_level: Optional[str],
|
|
proficiency: Optional[int],
|
|
) -> str:
|
|
raw = (level or proficiency_level or "").strip().lower()
|
|
if raw in LEVEL_HOURS_MULTIPLIER:
|
|
return raw
|
|
score = int(proficiency or 0)
|
|
if score >= 85:
|
|
return "expert"
|
|
if score >= 70:
|
|
return "advanced"
|
|
if score >= 50:
|
|
return "intermediate"
|
|
return "beginner"
|
|
|
|
level_map: dict[int, str] = {
|
|
int(skill_tag_id): normalize_level(level, proficiency_level, proficiency)
|
|
for skill_tag_id, level, proficiency_level, proficiency in skill_rows
|
|
}
|
|
|
|
def effective_estimated_hours(task: Task) -> float:
|
|
base = float(task.estimated_hours or 0.0)
|
|
if base <= 0:
|
|
return 0.0
|
|
required_ids = _safe_int_list(task.required_skill_tag_ids)
|
|
if not required_ids:
|
|
return base
|
|
multipliers = [
|
|
LEVEL_HOURS_MULTIPLIER.get(level_map[sid], 1.0)
|
|
for sid in required_ids
|
|
if sid in level_map
|
|
]
|
|
if not multipliers:
|
|
return base
|
|
factor = sum(multipliers) / len(multipliers)
|
|
return round(base * factor, 2)
|
|
|
|
workload.current_assigned_hours = round(
|
|
sum(effective_estimated_hours(task) for task in task_rows),
|
|
2,
|
|
)
|
|
|
|
await recalculate_user_workload_ratio(db, tenant_id, user_id, commit=False)
|
|
await db.commit()
|
|
await db.refresh(workload)
|
|
return workload
|
|
|