56 lines
2.2 KiB
Python
56 lines
2.2 KiB
Python
"""
|
|
Единый профиль ученика — источник правды для аналитики и повторения.
|
|
Никакой LLM здесь нет — только детерминированная бизнес-логика.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
from decimal import Decimal
|
|
|
|
|
|
async def get_weak_skills(conn, user_id: int, limit: int = 5):
|
|
"""Получить слабые навыки (mastery < 75 или needs_review/overdue или repeated_error).
|
|
Одинаково используется в /analytics и /review."""
|
|
rows = await conn.fetch("""
|
|
SELECT skill_tag, mastery_score, repeated_error_count,
|
|
retention_status, trend, recent_accuracy,
|
|
recognition_score, production_score, application_score,
|
|
median_correct_time_ms, days_since_last_attempt
|
|
FROM user_skill_mastery
|
|
WHERE user_id = $1 AND attempts_count >= 2
|
|
AND (mastery_score < 75
|
|
OR retention_status IN ('needs_review', 'overdue')
|
|
OR repeated_error_count > 0)
|
|
ORDER BY mastery_score ASC
|
|
LIMIT $2
|
|
""", user_id, limit)
|
|
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def get_strong_skills(conn, user_id: int, limit: int = 8):
|
|
"""Получить сильные навыки (mastery >= 75)."""
|
|
rows = await conn.fetch("""
|
|
SELECT skill_tag, mastery_score FROM user_skill_mastery
|
|
WHERE user_id = $1 AND attempts_count >= 2 AND mastery_score >= 75
|
|
ORDER BY mastery_score DESC
|
|
LIMIT $2
|
|
""", user_id, limit)
|
|
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_profile_hash(weak_skills: list) -> str:
|
|
"""Хэш списка слабых навыков — для инвалидации кэша."""
|
|
# Конвертируем Decimal в float для JSON
|
|
serializable = []
|
|
for s in weak_skills:
|
|
item = {}
|
|
for k, v in s.items():
|
|
if isinstance(v, Decimal):
|
|
item[k] = float(v)
|
|
else:
|
|
item[k] = v
|
|
serializable.append(item)
|
|
|
|
canonical = json.dumps(serializable, sort_keys=True, ensure_ascii=False)
|
|
return hashlib.sha256(canonical.encode()).hexdigest()[:16] |