2.0.0 Alpha: Data Refinery

This commit is contained in:
2026-08-08 21:31:56 +08:00
parent fa75081d4d
commit 562775e5db
48 changed files with 4172 additions and 661 deletions
+46 -54
View File
@@ -1,19 +1,10 @@
from web.database import query_db
from web.services.web_service import WebService
import json
from web.services.team_context_service import TeamContextService
class OpponentService:
@staticmethod
def _get_active_roster_ids():
lineups = WebService.get_lineups()
active_roster_ids = []
if lineups:
try:
raw_ids = json.loads(lineups[0]['player_ids_json'])
active_roster_ids = [str(uid) for uid in raw_ids]
except:
pass
return active_roster_ids
return TeamContextService.get_active_roster_ids()
@staticmethod
def get_opponent_list(page=1, per_page=20, sort_by='matches', search=None):
@@ -21,30 +12,21 @@ class OpponentService:
if not roster_ids:
return [], 0
# Placeholders
roster_ph = ','.join('?' for _ in roster_ids)
# 1. Identify Matches involving our roster (at least 1 member? usually 2 for 'team' match)
# Let's say at least 1 for broader coverage as requested ("1 match sample")
# But "Our Team" usually implies the entity. Let's stick to matches where we can identify "Us".
# If we use >=1, we catch solo Q matches of roster members. The user said "Non-team members or 1 match sample",
# but implied "facing different our team lineups".
# Let's use the standard "candidate matches" logic (>=2 roster members) to represent "The Team".
# OR, if user wants "Opponent Analysis" for even 1 match, maybe they mean ANY match in DB?
# "Left Top add Opponent Analysis... (non-team member or 1 sample)"
# This implies we analyze PLAYERS who are NOT us.
# Let's stick to matches where >= 1 roster member played, to define "Us" vs "Them".
# Actually, let's look at ALL matches in DB, and any player NOT in active roster is an "Opponent".
# This covers "1 sample".
# Query:
# Select all players who are NOT in active roster.
# Group by steam_id.
# Aggregate stats.
where_clauses = [f"CAST(mp.steam_id_64 AS TEXT) NOT IN ({roster_ph})"]
args = list(roster_ids)
where_clauses = [
f"CAST(mp.steam_id_64 AS TEXT) NOT IN ({roster_ph})",
f"""
EXISTS (
SELECT 1
FROM fact_match_players roster_mp
WHERE roster_mp.match_id = mp.match_id
AND CAST(roster_mp.steam_id_64 AS TEXT) IN ({roster_ph})
AND roster_mp.team_id != mp.team_id
)
""",
]
args = list(roster_ids) + list(roster_ids)
if search:
where_clauses.append("(LOWER(p.username) LIKE LOWER(?) OR mp.steam_id_64 LIKE ?)")
@@ -61,16 +43,6 @@ class OpponentService:
elif sort_by == 'win_rate':
sort_sql = "win_rate DESC"
# Main Aggregation Query
# We need to join fact_matches to get match info (win/loss, elo) if needed,
# but fact_match_players has is_win (boolean) usually? No, it has team_id.
# We need to determine if THEY won.
# fact_match_players doesn't store is_win directly in schema (I should check schema, but stats_service calculates it).
# Wait, stats_service.get_player_trend uses `mp.is_win`?
# Let's check schema. `fact_match_players` usually has `match_id`, `team_id`.
# `fact_matches` has `winner_team`.
# So we join.
offset = (page - 1) * per_page
sql = f"""
@@ -151,10 +123,17 @@ class OpponentService:
LEFT JOIN fact_match_teams fmt_gid ON mp.match_id = fmt_gid.match_id AND fmt_gid.group_id = mp.team_id
LEFT JOIN fact_match_teams fmt_tid ON mp.match_id = fmt_tid.match_id AND fmt_tid.group_tid = mp.match_team_id
WHERE CAST(mp.steam_id_64 AS TEXT) NOT IN ({roster_ph})
AND EXISTS (
SELECT 1
FROM fact_match_players roster_mp
WHERE roster_mp.match_id = mp.match_id
AND CAST(roster_mp.steam_id_64 AS TEXT) IN ({roster_ph})
AND roster_mp.team_id != mp.team_id
)
GROUP BY mp.steam_id_64
"""
rows = query_db('l2', sql, roster_ids)
rows = query_db('l2', sql, roster_ids + roster_ids)
# Initialize Buckets
elo_buckets = {'<1000': 0, '1000-1200': 0, '1200-1400': 0, '1400-1600': 0, '1600-1800': 0, '1800-2000': 0, '>2000': 0}
@@ -216,13 +195,12 @@ class OpponentService:
player = dict(info)
player['avatar_url'] = StatsService.resolve_avatar_url(steam_id, player.get('avatar_url'))
# 2. Match History vs Us (All matches this player played)
# We define "Us" as matches where this player is an opponent.
# But actually, we just show ALL their matches in our DB, assuming our DB only contains matches relevant to us?
# Usually yes, but if we have a huge DB, we might want to filter by "Contains Roster Member".
# For now, show all matches in DB for this player.
sql_history = """
roster_ids = OpponentService._get_active_roster_ids()
if not roster_ids:
return None
roster_ph = ','.join('?' for _ in roster_ids)
sql_history = f"""
SELECT
m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team,
mp.team_id, mp.match_team_id, mp.rating, mp.kd_ratio, mp.adr, mp.kills, mp.deaths,
@@ -236,9 +214,16 @@ class OpponentService:
LEFT JOIN fact_match_teams fmt_gid ON mp.match_id = fmt_gid.match_id AND fmt_gid.group_id = mp.team_id
LEFT JOIN fact_match_teams fmt_tid ON mp.match_id = fmt_tid.match_id AND fmt_tid.group_tid = mp.match_team_id
WHERE mp.steam_id_64 = ?
AND EXISTS (
SELECT 1
FROM fact_match_players roster_mp
WHERE roster_mp.match_id = mp.match_id
AND CAST(roster_mp.steam_id_64 AS TEXT) IN ({roster_ph})
AND roster_mp.team_id != mp.team_id
)
ORDER BY m.start_time DESC
"""
history = query_db('l2', sql_history, [steam_id])
history = query_db('l2', sql_history, [steam_id] + roster_ids)
# 3. Aggregation by ELO
elo_buckets = {
@@ -389,11 +374,18 @@ class OpponentService:
LEFT JOIN fact_match_teams fmt_gid ON mp.match_id = fmt_gid.match_id AND fmt_gid.group_id = mp.team_id
LEFT JOIN fact_match_teams fmt_tid ON mp.match_id = fmt_tid.match_id AND fmt_tid.group_tid = mp.match_team_id
WHERE CAST(mp.steam_id_64 AS TEXT) NOT IN ({roster_ph})
AND EXISTS (
SELECT 1
FROM fact_match_players roster_mp
WHERE roster_mp.match_id = mp.match_id
AND CAST(roster_mp.steam_id_64 AS TEXT) IN ({roster_ph})
AND roster_mp.team_id != mp.team_id
)
AND m.map_name IS NOT NULL AND m.map_name <> ''
GROUP BY m.map_name
ORDER BY matches DESC
"""
rows = query_db('l2', sql, roster_ids)
rows = query_db('l2', sql, roster_ids + roster_ids)
results = []
for r in rows:
d = dict(r)