101 lines
3.1 KiB
Python
101 lines
3.1 KiB
Python
"""유저별 포인트 누적 — grade_prediction 의 (key, value) 를 user_points 에 반영.
|
|
|
|
채점(apply_result)에서 호출. 결과 정정(재채점)에도 안전하도록 영향받은
|
|
이메일의 채점 가능한 픽 전체를 재집계해 upsert 한다(idempotent — 몇 번을
|
|
다시 돌려도 같은 결과, 증분 방식의 이중 누적 위험 없음).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ..domain import now_utc
|
|
from ..models import Match, UserPoints, UserPrediction
|
|
from ..scoring import grade_prediction
|
|
|
|
log = logging.getLogger("triplepick.points")
|
|
|
|
# scoring.json 의 key → user_points 등급별 횟수 컬럼
|
|
_KEY_TO_COL = {
|
|
"score_exact": "exact_count",
|
|
"score_close": "close_count",
|
|
"score_outcome": "outcome_count",
|
|
"score_partial": "partial_count",
|
|
"score_miss": "miss_count",
|
|
}
|
|
|
|
|
|
def _empty() -> dict:
|
|
return {
|
|
"total_points": 0,
|
|
"exact_count": 0,
|
|
"close_count": 0,
|
|
"outcome_count": 0,
|
|
"partial_count": 0,
|
|
"miss_count": 0,
|
|
"matches_played": 0,
|
|
"first_scored_at": None,
|
|
}
|
|
|
|
|
|
async def accumulate_user_points(
|
|
db: AsyncSession, emails: set[str | None]
|
|
) -> int:
|
|
"""이메일별 누적 포인트 재집계 → user_points upsert. 갱신 행 수 반환.
|
|
|
|
커밋은 호출자(apply_result) 책임 — 채점과 누적이 한 트랜잭션으로 묶인다.
|
|
"""
|
|
targets = {e.strip().lower() for e in emails if e and e.strip()}
|
|
if not targets:
|
|
return 0
|
|
|
|
rows = (
|
|
await db.execute(
|
|
select(UserPrediction, Match)
|
|
.join(Match, UserPrediction.match_id == Match.match_id)
|
|
.where(
|
|
func.lower(UserPrediction.email).in_(targets),
|
|
Match.result_score_a.is_not(None),
|
|
Match.result_score_b.is_not(None),
|
|
)
|
|
)
|
|
).all()
|
|
|
|
agg: dict[str, dict] = {e: _empty() for e in targets}
|
|
for pick, match in rows:
|
|
key, value = grade_prediction(
|
|
pick.score_a, pick.score_b, match.result_score_a, match.result_score_b,
|
|
baseball=match.league in ("kbo", "mlb"),
|
|
)
|
|
t = agg[pick.email.strip().lower()]
|
|
t["total_points"] += value
|
|
t[_KEY_TO_COL[key]] += 1
|
|
t["matches_played"] += 1
|
|
ts = pick.scored_at or pick.created_at
|
|
if ts and (t["first_scored_at"] is None or ts < t["first_scored_at"]):
|
|
t["first_scored_at"] = ts
|
|
|
|
existing = {
|
|
up.email: up
|
|
for up in (
|
|
await db.execute(select(UserPoints).where(UserPoints.email.in_(targets)))
|
|
).scalars()
|
|
}
|
|
for email, t in agg.items():
|
|
row = existing.get(email)
|
|
if row is None:
|
|
row = UserPoints(email=email)
|
|
db.add(row)
|
|
for col, val in t.items():
|
|
setattr(row, col, val)
|
|
row.updated_at = now_utc()
|
|
|
|
log.info(
|
|
"user_points: %d명 누적 갱신 (%s)",
|
|
len(agg),
|
|
", ".join(f"{e}={t['total_points']}p" for e, t in agg.items()),
|
|
)
|
|
return len(agg)
|