155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
"""주간 회고 채점 통합 잡 — 일요일 03:00 KST 실행.
|
|
|
|
1) 기존 purchase_manager.check_purchases_for_draw() 로 사용자 구매 자동 채점
|
|
2) 큐레이터 4계층 picks vs 추첨 결과 비교
|
|
3) 패턴 요약·갭 계산
|
|
4) weekly_review UPSERT
|
|
5) 4등 이상 발견 시 agent-office webhook 호출
|
|
"""
|
|
import json
|
|
import logging
|
|
import os
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
|
|
from .. import db
|
|
from ..purchase_manager import check_purchases_for_draw
|
|
from .grading_helpers import (
|
|
score_picks_against_draw,
|
|
summarize_pattern,
|
|
aggregate_pattern_summaries,
|
|
compute_pattern_delta,
|
|
)
|
|
|
|
logger = logging.getLogger("lotto-backend")
|
|
|
|
AGENT_OFFICE_URL = os.environ.get("AGENT_OFFICE_URL", "http://agent-office:8000")
|
|
|
|
|
|
def _flatten_curator_picks(briefing: dict) -> list:
|
|
"""4계층 picks 를 모두 합쳐 단일 리스트(score 계산용)."""
|
|
picks = briefing.get("picks") or {}
|
|
if isinstance(picks, list):
|
|
return picks
|
|
out = []
|
|
for tier in ("core", "bonus", "extended", "pool"):
|
|
out.extend(picks.get(tier) or [])
|
|
return out
|
|
|
|
|
|
def _curator_score(briefing: dict, win_nums: list, bonus: int) -> dict:
|
|
if not briefing:
|
|
return {}
|
|
flat = _flatten_curator_picks(briefing)
|
|
if not flat:
|
|
return {}
|
|
return score_picks_against_draw(flat, win_nums, bonus)
|
|
|
|
|
|
def _user_score(drw_no: int, win_nums: list) -> dict:
|
|
purchases = db.get_purchases(draw_no=drw_no)
|
|
if not purchases:
|
|
return {}
|
|
matches = []
|
|
win_set = set(win_nums)
|
|
pattern_summaries = []
|
|
for p in purchases:
|
|
for nums in (p.get("numbers") or []):
|
|
if not nums:
|
|
continue
|
|
m = len(set(nums) & win_set)
|
|
matches.append(m)
|
|
pattern_summaries.append(summarize_pattern(nums))
|
|
if not matches:
|
|
return {}
|
|
return {
|
|
"avg_match": round(sum(matches) / len(matches), 2),
|
|
"best_match": max(matches),
|
|
"five_plus_prizes": sum(1 for m in matches if m >= 3),
|
|
"pattern_avg": aggregate_pattern_summaries(pattern_summaries),
|
|
}
|
|
|
|
|
|
def _trigger_prize_alert(drw_no: int, match_count: int, numbers: list, purchase_id: int) -> None:
|
|
try:
|
|
with httpx.Client(timeout=10) as client:
|
|
client.post(
|
|
f"{AGENT_OFFICE_URL}/api/agent-office/notify/lotto-prize",
|
|
json={
|
|
"draw_no": drw_no,
|
|
"match_count": match_count,
|
|
"numbers": numbers,
|
|
"purchase_id": purchase_id,
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"[grade_weekly_review] prize alert webhook failed: {e}")
|
|
|
|
|
|
def run_weekly_grading(drw_no: int) -> dict:
|
|
"""주어진 회차에 대해 채점 잡 1회 실행. 멱등."""
|
|
draw = db.get_draw(drw_no)
|
|
if not draw:
|
|
logger.warning(f"[grade_weekly_review] draw {drw_no} not found, skip")
|
|
return {"ok": False, "reason": "no draw"}
|
|
|
|
win_nums = [draw["n1"], draw["n2"], draw["n3"], draw["n4"], draw["n5"], draw["n6"]]
|
|
bonus = draw["bonus"]
|
|
|
|
# 1) 사용자 구매 자동 채점 (기존 인프라)
|
|
try:
|
|
check_purchases_for_draw(drw_no)
|
|
except Exception as e:
|
|
logger.warning(f"[grade_weekly_review] check_purchases_for_draw failed: {e}")
|
|
|
|
# 2) 4등 이상 발견 시 webhook
|
|
purchases = db.get_purchases(draw_no=drw_no, checked=True)
|
|
for p in purchases:
|
|
for r in (p.get("results") or []):
|
|
if r.get("correct", 0) >= 4:
|
|
_trigger_prize_alert(drw_no, r["correct"], r["numbers"], p["id"])
|
|
|
|
# 3) 큐레이터 자기 평가
|
|
briefing = db.get_briefing(drw_no)
|
|
cur = _curator_score(briefing, win_nums, bonus)
|
|
|
|
# 4) 사용자 평가 (재로드, 구매가 다 채점된 후 패턴 계산)
|
|
usr = _user_score(drw_no, win_nums)
|
|
|
|
# 5) 추첨 패턴 요약 + 델타
|
|
draw_summary = summarize_pattern(win_nums)
|
|
draw_pattern = {
|
|
"low_avg": draw_summary["low_count"],
|
|
"odd_avg": draw_summary["odd_count"],
|
|
"sum_avg": draw_summary["sum"],
|
|
}
|
|
user_pattern = usr.get("pattern_avg", {})
|
|
delta = compute_pattern_delta(user_pattern, draw_pattern) if user_pattern else ""
|
|
|
|
# 6) UPSERT
|
|
payload = {
|
|
"draw_no": drw_no,
|
|
"curator_avg_match": cur.get("avg_match"),
|
|
"curator_best_tier": cur.get("best_tier"),
|
|
"curator_best_match": cur.get("best_match"),
|
|
"curator_5plus_prizes": cur.get("five_plus_prizes"),
|
|
"user_avg_match": usr.get("avg_match"),
|
|
"user_best_match": usr.get("best_match"),
|
|
"user_5plus_prizes": usr.get("five_plus_prizes"),
|
|
"user_pattern_summary": json.dumps(user_pattern, ensure_ascii=False) if user_pattern else None,
|
|
"draw_pattern_summary": json.dumps(draw_pattern, ensure_ascii=False),
|
|
"pattern_delta": delta,
|
|
}
|
|
rid = db.save_review(payload)
|
|
logger.info(f"[grade_weekly_review] saved review id={rid} for draw {drw_no}")
|
|
return {"ok": True, "review_id": rid}
|
|
|
|
|
|
def run_for_latest() -> dict:
|
|
"""가장 최근 sync된 추첨 회차로 채점 — cron 진입점."""
|
|
latest = db.get_latest_draw()
|
|
if not latest:
|
|
return {"ok": False, "reason": "no draws"}
|
|
return run_weekly_grading(latest["drw_no"])
|