Files
yrtv/web/services/feature_service.py

257 lines
12 KiB
Python
Raw Normal View History

2026-01-26 02:13:06 +08:00
from web.database import query_db
class FeatureService:
@staticmethod
def get_player_features(steam_id):
sql = "SELECT * FROM dm_player_features WHERE steam_id_64 = ?"
return query_db('l3', sql, [steam_id], one=True)
@staticmethod
def get_players_list(page=1, per_page=20, sort_by='rating', search=None):
offset = (page - 1) * per_page
# Sort Mapping
sort_map = {
'rating': 'basic_avg_rating',
'kd': 'basic_avg_kd',
'kast': 'basic_avg_kast',
'matches': 'matches_played'
}
order_col = sort_map.get(sort_by, 'basic_avg_rating')
from web.services.stats_service import StatsService
# Helper to attach match counts
def attach_match_counts(player_list):
if not player_list:
return
ids = [p['steam_id_64'] for p in player_list]
# Batch query for counts from L2
placeholders = ','.join('?' for _ in ids)
sql = f"""
SELECT steam_id_64, COUNT(*) as cnt
FROM fact_match_players
WHERE steam_id_64 IN ({placeholders})
GROUP BY steam_id_64
"""
counts = query_db('l2', sql, ids)
cnt_dict = {r['steam_id_64']: r['cnt'] for r in counts}
for p in player_list:
p['matches_played'] = cnt_dict.get(p['steam_id_64'], 0)
if search:
# ... existing search logic ...
# Get all matching players
l2_players, _ = StatsService.get_players(page=1, per_page=100, search=search)
if not l2_players:
return [], 0
# ... (Merge logic) ...
# I need to insert the match count logic inside the merge loop or after
steam_ids = [p['steam_id_64'] for p in l2_players]
placeholders = ','.join('?' for _ in steam_ids)
sql = f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({placeholders})"
features = query_db('l3', sql, steam_ids)
f_dict = {f['steam_id_64']: f for f in features}
# Get counts for sorting
count_sql = f"SELECT steam_id_64, COUNT(*) as cnt FROM fact_match_players WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64"
counts = query_db('l2', count_sql, steam_ids)
cnt_dict = {r['steam_id_64']: r['cnt'] for r in counts}
merged = []
for p in l2_players:
f = f_dict.get(p['steam_id_64'])
m = dict(p)
if f:
m.update(dict(f))
else:
# Fallback Calc
stats = StatsService.get_player_basic_stats(p['steam_id_64'])
if stats:
m['basic_avg_rating'] = stats['rating']
m['basic_avg_kd'] = stats['kd']
m['basic_avg_kast'] = stats['kast']
else:
m['basic_avg_rating'] = 0
m['basic_avg_kd'] = 0
m['basic_avg_kast'] = 0 # Ensure kast exists
m['matches_played'] = cnt_dict.get(p['steam_id_64'], 0)
merged.append(m)
merged.sort(key=lambda x: x.get(order_col, 0) or 0, reverse=True)
total = len(merged)
start = (page - 1) * per_page
end = start + per_page
return merged[start:end], total
else:
# Browse mode
# Check L3
l3_count = query_db('l3', "SELECT COUNT(*) as cnt FROM dm_player_features", one=True)['cnt']
if l3_count == 0 or sort_by == 'matches':
# If sorting by matches, we MUST use L2 counts because L3 might not have it or we want dynamic.
# OR if L3 is empty.
# Since L3 schema is unknown regarding 'matches_played', let's assume we fallback to L2 logic
# but paginated in memory if dataset is small, or just fetch all L2 players?
# Fetching all L2 players is bad if many.
# But for 'matches' sort, we need to know counts for ALL to sort correctly.
# Solution: Query L2 for top N players by match count.
if sort_by == 'matches':
# Query L2 for IDs ordered by count
sql = """
SELECT steam_id_64, COUNT(*) as cnt
FROM fact_match_players
GROUP BY steam_id_64
ORDER BY cnt DESC
LIMIT ? OFFSET ?
"""
top_ids = query_db('l2', sql, [per_page, offset])
if not top_ids:
return [], 0
total = query_db('l2', "SELECT COUNT(DISTINCT steam_id_64) as cnt FROM fact_match_players", one=True)['cnt']
ids = [r['steam_id_64'] for r in top_ids]
# Fetch details for these IDs
l2_players = StatsService.get_players_by_ids(ids)
# Merge logic (reuse)
merged = []
# Fetch L3 features for these IDs to show stats
p_ph = ','.join('?' for _ in ids)
f_sql = f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({p_ph})"
features = query_db('l3', f_sql, ids)
f_dict = {f['steam_id_64']: f for f in features}
cnt_dict = {r['steam_id_64']: r['cnt'] for r in top_ids}
# Map L2 players to dict for easy access (though list order matters for sort?)
# Actually top_ids is sorted.
p_dict = {p['steam_id_64']: p for p in l2_players}
for r in top_ids: # Preserve order
sid = r['steam_id_64']
p = p_dict.get(sid)
if not p: continue
m = dict(p)
f = f_dict.get(sid)
if f:
m.update(dict(f))
else:
stats = StatsService.get_player_basic_stats(sid)
if stats:
m['basic_avg_rating'] = stats['rating']
m['basic_avg_kd'] = stats['kd']
m['basic_avg_kast'] = stats['kast']
else:
m['basic_avg_rating'] = 0
m['basic_avg_kd'] = 0
m['basic_avg_kast'] = 0
m['matches_played'] = r['cnt']
merged.append(m)
return merged, total
# L3 empty fallback (existing logic)
l2_players, total = StatsService.get_players(page, per_page, sort_by=None)
merged = []
attach_match_counts(l2_players) # Helper
for p in l2_players:
m = dict(p)
stats = StatsService.get_player_basic_stats(p['steam_id_64'])
if stats:
m['basic_avg_rating'] = stats['rating']
m['basic_avg_kd'] = stats['kd']
m['basic_avg_kast'] = stats['kast']
else:
m['basic_avg_rating'] = 0
m['basic_avg_kd'] = 0
m['basic_avg_kast'] = 0
m['matches_played'] = p.get('matches_played', 0)
merged.append(m)
if sort_by != 'rating':
merged.sort(key=lambda x: x.get(order_col, 0) or 0, reverse=True)
return merged, total
# Normal L3 browse (sort by rating/kd/kast)
sql = f"SELECT * FROM dm_player_features ORDER BY {order_col} DESC LIMIT ? OFFSET ?"
features = query_db('l3', sql, [per_page, offset])
total = query_db('l3', "SELECT COUNT(*) as cnt FROM dm_player_features", one=True)['cnt']
if not features:
return [], total
steam_ids = [f['steam_id_64'] for f in features]
l2_players = StatsService.get_players_by_ids(steam_ids)
p_dict = {p['steam_id_64']: p for p in l2_players}
merged = []
for f in features:
m = dict(f)
p = p_dict.get(f['steam_id_64'])
if p:
m.update(dict(p))
else:
m['username'] = f['steam_id_64'] # Fallback
m['avatar_url'] = None
merged.append(m)
return merged, total
@staticmethod
def get_top_players(limit=20, sort_by='basic_avg_rating'):
# Safety check for sort_by to prevent injection
allowed_sorts = ['basic_avg_rating', 'basic_avg_kd', 'basic_avg_kast', 'basic_avg_rws']
if sort_by not in allowed_sorts:
sort_by = 'basic_avg_rating'
sql = f"""
SELECT f.*, p.username, p.avatar_url
FROM dm_player_features f
LEFT JOIN l2.dim_players p ON f.steam_id_64 = p.steam_id_64
ORDER BY {sort_by} DESC
LIMIT ?
"""
# Note: Cross-database join (l2.dim_players) works in SQLite if attached.
# But `query_db` connects to one DB.
# Strategy: Fetch features, then fetch player infos from L2. Or attach DB.
# Simple strategy: Fetch features, then extract steam_ids and batch fetch from L2 in StatsService.
# Or simpler: Just return features and let the controller/template handle the name/avatar via another call or pre-fetching.
# Actually, for "Player List" view, we really want L3 data joined with L2 names.
# I will change this to just return features for now, and handle joining in the route handler or via a helper that attaches databases.
# Attaching is better.
return query_db('l3', f"SELECT * FROM dm_player_features ORDER BY {sort_by} DESC LIMIT ?", [limit])
@staticmethod
def get_player_trend(steam_id, limit=30):
# This requires `fact_match_features` or querying L2 matches for historical data.
# WebRDD says: "Trend graph: Recent 10/20 matches Rating trend (Chart.js)."
# We can get this from L2 fact_match_players.
sql = """
SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id
FROM fact_match_players mp
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.steam_id_64 = ?
ORDER BY m.start_time DESC
LIMIT ?
"""
# This query needs to run against L2.
# So this method should actually be in StatsService or FeatureService connecting to L2.
# I will put it here but note it uses L2. Actually, better to put in StatsService if it uses L2 tables.
# But FeatureService conceptualizes "Trends". I'll move it to StatsService for implementation correctness (DB context).
pass