2.0.0-rc1 : Profile and achievements update.
This commit is contained in:
@@ -246,12 +246,19 @@ class IntegrityService:
|
||||
@staticmethod
|
||||
def _check_l3(db, roster_ids, checks, counts):
|
||||
required_tables = {
|
||||
'dm_duo_stats',
|
||||
'dm_match_player_reports',
|
||||
'dm_match_reports',
|
||||
'dm_player_features',
|
||||
'dm_player_awards',
|
||||
'dm_player_match_history',
|
||||
'dm_player_map_stats',
|
||||
'dm_player_period_stats',
|
||||
'dm_player_record_events',
|
||||
'dm_player_records',
|
||||
'dm_player_weapon_stats',
|
||||
'dm_lineup_stats',
|
||||
'dm_team_season_stats',
|
||||
}
|
||||
missing = sorted(required_tables - IntegrityService._table_names(db))
|
||||
IntegrityService._check(
|
||||
@@ -281,6 +288,42 @@ class IntegrityService:
|
||||
counts['l3_records'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_player_records'
|
||||
).fetchone()[0]
|
||||
counts['l3_duos'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_duo_stats'
|
||||
).fetchone()[0]
|
||||
counts['l3_lineups'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_lineup_stats'
|
||||
).fetchone()[0]
|
||||
counts['l3_match_reports'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_match_reports'
|
||||
).fetchone()[0]
|
||||
counts['l3_player_reports'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_match_player_reports'
|
||||
).fetchone()[0]
|
||||
counts['l3_record_events'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_player_record_events'
|
||||
).fetchone()[0]
|
||||
counts['l3_seasons'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_team_season_stats'
|
||||
).fetchone()[0]
|
||||
counts['l3_awards'] = db.execute(
|
||||
'SELECT COUNT(*) FROM dm_player_awards'
|
||||
).fetchone()[0]
|
||||
expected_matches = counts.get('matches', 0)
|
||||
IntegrityService._check(
|
||||
checks,
|
||||
'Post-match report coverage',
|
||||
(
|
||||
'pass'
|
||||
if counts['l3_match_reports'] == expected_matches
|
||||
else 'fail'
|
||||
),
|
||||
(
|
||||
f"{counts['l3_match_reports']}/"
|
||||
f"{expected_matches} matches reported"
|
||||
),
|
||||
counts['l3_match_reports'],
|
||||
)
|
||||
|
||||
if roster_ids:
|
||||
placeholders = ','.join('?' for _ in roster_ids)
|
||||
@@ -323,6 +366,21 @@ class IntegrityService:
|
||||
f'{actual_history}/{expected_history} player-match rows materialized',
|
||||
actual_history,
|
||||
)
|
||||
player_report_count = db.execute(
|
||||
f"""
|
||||
SELECT COUNT(*)
|
||||
FROM dm_match_player_reports
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
""",
|
||||
roster_ids,
|
||||
).fetchone()[0]
|
||||
IntegrityService._check(
|
||||
checks,
|
||||
'Roster player-report completeness',
|
||||
'pass' if player_report_count == expected_history else 'fail',
|
||||
f'{player_report_count}/{expected_history} player reports',
|
||||
player_report_count,
|
||||
)
|
||||
|
||||
score_rows = db.execute(
|
||||
f"""
|
||||
@@ -378,6 +436,13 @@ class IntegrityService:
|
||||
('l3_weapons', 'Player weapon stats mart'),
|
||||
('l3_periods', 'Player period stats mart'),
|
||||
('l3_records', 'Player records mart'),
|
||||
('l3_duos', 'Team duo stats mart'),
|
||||
('l3_lineups', 'Team lineup stats mart'),
|
||||
('l3_match_reports', 'Post-match report mart'),
|
||||
('l3_player_reports', 'Player post-match report mart'),
|
||||
('l3_record_events', 'Record event mart'),
|
||||
('l3_seasons', 'Team season mart'),
|
||||
('l3_awards', 'Player award mart'),
|
||||
):
|
||||
value = counts[key]
|
||||
IntegrityService._check(
|
||||
@@ -424,6 +489,8 @@ class IntegrityService:
|
||||
'schema_migrations',
|
||||
'strategy_boards',
|
||||
'team_lineups',
|
||||
'team_roster_members',
|
||||
'team_roster_versions',
|
||||
'wiki_pages',
|
||||
}
|
||||
missing = sorted(required_tables - IntegrityService._table_names(db))
|
||||
@@ -509,3 +576,42 @@ class IntegrityService:
|
||||
f'{active_count} active lineups configured',
|
||||
active_count,
|
||||
)
|
||||
|
||||
current_versions = db.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM team_roster_versions
|
||||
WHERE is_current = 1
|
||||
"""
|
||||
).fetchall()
|
||||
current_member_count = 0
|
||||
if len(current_versions) == 1:
|
||||
current_member_count = db.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM team_roster_members
|
||||
WHERE roster_version_id = ?
|
||||
""",
|
||||
[current_versions[0]['id']],
|
||||
).fetchone()[0]
|
||||
counts['roster_versions'] = db.execute(
|
||||
'SELECT COUNT(*) FROM team_roster_versions'
|
||||
).fetchone()[0]
|
||||
counts['current_roster_members'] = current_member_count
|
||||
IntegrityService._check(
|
||||
checks,
|
||||
'Current roster version',
|
||||
'pass' if len(current_versions) == 1 else 'fail',
|
||||
f'{len(current_versions)} current roster versions',
|
||||
len(current_versions),
|
||||
)
|
||||
IntegrityService._check(
|
||||
checks,
|
||||
'Roster version membership',
|
||||
'pass' if current_member_count == counts.get('active_roster', 0) else 'fail',
|
||||
(
|
||||
f"{current_member_count}/"
|
||||
f"{counts.get('active_roster', 0)} active members versioned"
|
||||
),
|
||||
current_member_count,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import json
|
||||
|
||||
from web.database import query_db
|
||||
|
||||
|
||||
class NarrativeService:
|
||||
AWARD_TYPE_LABELS = {
|
||||
'daily': '单日最佳',
|
||||
'weekly': '星期最佳',
|
||||
'monthly': '月度最佳',
|
||||
'quarterly': '季度最佳',
|
||||
'yearly': '年度最佳',
|
||||
}
|
||||
|
||||
PERFORMANCE_LABELS = {
|
||||
'surge': '状态爆发',
|
||||
'above_form': '高于近期',
|
||||
'stable': '稳定发挥',
|
||||
'below_form': '低于近期',
|
||||
'slump': '状态低迷',
|
||||
'insufficient_sample': '样本不足',
|
||||
}
|
||||
|
||||
RECORD_LABELS = {
|
||||
'highest_rating': '最高 Rating',
|
||||
'most_kills': '最多击杀',
|
||||
'highest_adr': '最高 ADR',
|
||||
'highest_kd': '最高 K/D',
|
||||
'most_headshots': '最多爆头',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _identity_map(steam_ids):
|
||||
steam_ids = sorted({str(value) for value in steam_ids if value})
|
||||
if not steam_ids:
|
||||
return {}
|
||||
placeholders = ','.join('?' for _ in steam_ids)
|
||||
rows = query_db(
|
||||
'l2',
|
||||
f"""
|
||||
SELECT steam_id_64, username, avatar_url
|
||||
FROM dim_players
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
""",
|
||||
steam_ids,
|
||||
)
|
||||
return {str(row['steam_id_64']): dict(row) for row in rows}
|
||||
|
||||
@staticmethod
|
||||
def get_match_report(match_id):
|
||||
report = query_db(
|
||||
'l3',
|
||||
'SELECT * FROM dm_match_reports WHERE match_id = ?',
|
||||
[match_id],
|
||||
one=True,
|
||||
)
|
||||
if not report:
|
||||
return None
|
||||
result = dict(report)
|
||||
player_rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_match_player_reports
|
||||
WHERE match_id = ?
|
||||
ORDER BY rating DESC
|
||||
""",
|
||||
[match_id],
|
||||
)
|
||||
player_reports = [dict(row) for row in player_rows]
|
||||
steam_ids = [row['steam_id_64'] for row in player_reports]
|
||||
steam_ids.extend([
|
||||
result.get('mvp_steam_id'),
|
||||
result.get('improver_steam_id'),
|
||||
])
|
||||
duo = json.loads(result.get('strongest_duo_json') or 'null')
|
||||
if duo:
|
||||
steam_ids.extend(duo.get('steam_ids') or [])
|
||||
identities = NarrativeService._identity_map(steam_ids)
|
||||
|
||||
for player in player_reports:
|
||||
player['identity'] = identities.get(
|
||||
str(player['steam_id_64']),
|
||||
{'username': player['steam_id_64']},
|
||||
)
|
||||
player['performance_text'] = NarrativeService.PERFORMANCE_LABELS.get(
|
||||
player['performance_label'],
|
||||
player['performance_label'],
|
||||
)
|
||||
record_keys = json.loads(player['record_keys_json'] or '[]')
|
||||
player['record_labels'] = [
|
||||
NarrativeService.RECORD_LABELS.get(key, key)
|
||||
for key in record_keys
|
||||
]
|
||||
result['players'] = player_reports
|
||||
result['mvp'] = identities.get(str(result.get('mvp_steam_id')), {})
|
||||
result['improver'] = identities.get(
|
||||
str(result.get('improver_steam_id')),
|
||||
{},
|
||||
)
|
||||
if duo:
|
||||
duo['players'] = [
|
||||
identities.get(str(steam_id), {'username': steam_id})
|
||||
for steam_id in duo.get('steam_ids', [])
|
||||
]
|
||||
result['strongest_duo'] = duo
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def list_match_reports(limit=30):
|
||||
rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_match_reports
|
||||
ORDER BY match_date DESC, match_id DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
[limit],
|
||||
)
|
||||
identities = NarrativeService._identity_map(
|
||||
[row['mvp_steam_id'] for row in rows]
|
||||
)
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['mvp'] = identities.get(str(item['mvp_steam_id']), {})
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def list_awards(award_type=None, limit=100):
|
||||
args = []
|
||||
where = ''
|
||||
if award_type in NarrativeService.AWARD_TYPE_LABELS:
|
||||
where = 'WHERE award_type = ?'
|
||||
args.append(award_type)
|
||||
args.append(limit)
|
||||
rows = query_db(
|
||||
'l3',
|
||||
f"""
|
||||
SELECT *
|
||||
FROM dm_player_awards
|
||||
{where}
|
||||
ORDER BY period_start DESC,
|
||||
CASE award_type
|
||||
WHEN 'yearly' THEN 1
|
||||
WHEN 'quarterly' THEN 2
|
||||
WHEN 'monthly' THEN 3
|
||||
WHEN 'weekly' THEN 4
|
||||
ELSE 5
|
||||
END
|
||||
LIMIT ?
|
||||
""",
|
||||
args,
|
||||
)
|
||||
identities = NarrativeService._identity_map(
|
||||
[row['steam_id_64'] for row in rows]
|
||||
)
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['identity'] = identities.get(str(item['steam_id_64']), {})
|
||||
item['award_label'] = NarrativeService.AWARD_TYPE_LABELS.get(
|
||||
item['award_type'],
|
||||
item['award_type'],
|
||||
)
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_award_summary():
|
||||
rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT
|
||||
steam_id_64,
|
||||
COUNT(*) AS awards,
|
||||
SUM(CASE WHEN award_type = 'daily' THEN 1 ELSE 0 END) AS daily,
|
||||
SUM(CASE WHEN award_type = 'weekly' THEN 1 ELSE 0 END) AS weekly,
|
||||
SUM(CASE WHEN award_type = 'monthly' THEN 1 ELSE 0 END) AS monthly,
|
||||
SUM(CASE WHEN award_type = 'quarterly' THEN 1 ELSE 0 END) AS quarterly,
|
||||
SUM(CASE WHEN award_type = 'yearly' THEN 1 ELSE 0 END) AS yearly
|
||||
FROM dm_player_awards
|
||||
GROUP BY steam_id_64
|
||||
ORDER BY yearly DESC, quarterly DESC, monthly DESC,
|
||||
weekly DESC, daily DESC
|
||||
""",
|
||||
)
|
||||
identities = NarrativeService._identity_map(
|
||||
[row['steam_id_64'] for row in rows]
|
||||
)
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['identity'] = identities.get(str(item['steam_id_64']), {})
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_seasons():
|
||||
rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_team_season_stats
|
||||
ORDER BY season_key DESC
|
||||
""",
|
||||
)
|
||||
identities = NarrativeService._identity_map(
|
||||
[row['top_player_steam_id'] for row in rows]
|
||||
)
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['top_player'] = identities.get(
|
||||
str(item['top_player_steam_id']),
|
||||
{},
|
||||
)
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_player_honors(steam_id, limit=20):
|
||||
rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_player_awards
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY period_start DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
[steam_id, limit],
|
||||
)
|
||||
awards = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['award_label'] = NarrativeService.AWARD_TYPE_LABELS.get(
|
||||
item['award_type'],
|
||||
item['award_type'],
|
||||
)
|
||||
awards.append(item)
|
||||
record_rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_player_record_events
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY match_date DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
[steam_id, limit],
|
||||
)
|
||||
record_events = []
|
||||
for row in record_rows:
|
||||
item = dict(row)
|
||||
item['record_label'] = NarrativeService.RECORD_LABELS.get(
|
||||
item['record_key'],
|
||||
item['record_key'],
|
||||
)
|
||||
record_events.append(item)
|
||||
return {
|
||||
'awards': awards,
|
||||
'award_count': len(awards),
|
||||
'record_events': record_events,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_player_identity(steam_id):
|
||||
feature = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT
|
||||
total_matches,
|
||||
first_match_date,
|
||||
last_match_date,
|
||||
core_top_weapon,
|
||||
meta_map_best_map,
|
||||
tier_percentile
|
||||
FROM dm_player_features
|
||||
WHERE steam_id_64 = ?
|
||||
""",
|
||||
[steam_id],
|
||||
one=True,
|
||||
)
|
||||
roster = query_db(
|
||||
'web',
|
||||
"""
|
||||
SELECT
|
||||
member.member_role,
|
||||
version.name AS roster_version,
|
||||
(
|
||||
SELECT MIN(history_version.effective_from)
|
||||
FROM team_roster_members history_member
|
||||
JOIN team_roster_versions history_version
|
||||
ON history_version.id = history_member.roster_version_id
|
||||
WHERE history_member.steam_id_64 = member.steam_id_64
|
||||
) AS effective_from
|
||||
FROM team_roster_members member
|
||||
JOIN team_roster_versions version
|
||||
ON version.id = member.roster_version_id
|
||||
WHERE member.steam_id_64 = ?
|
||||
ORDER BY version.is_current DESC, version.effective_from DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
[steam_id],
|
||||
one=True,
|
||||
)
|
||||
awards = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT award_type, COUNT(*) AS count
|
||||
FROM dm_player_awards
|
||||
WHERE steam_id_64 = ?
|
||||
GROUP BY award_type
|
||||
""",
|
||||
[steam_id],
|
||||
)
|
||||
result = dict(feature) if feature else {}
|
||||
if roster:
|
||||
result.update(dict(roster))
|
||||
result['awards_by_type'] = {
|
||||
row['award_type']: row['count'] for row in awards
|
||||
}
|
||||
result['award_count'] = sum(result['awards_by_type'].values())
|
||||
return result
|
||||
@@ -0,0 +1,249 @@
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
from web.config import Config
|
||||
|
||||
|
||||
class RosterVersionService:
|
||||
@staticmethod
|
||||
def _connect_web(web_db_path=None):
|
||||
db = sqlite3.connect(web_db_path or Config.DB_WEB_PATH, timeout=30)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute('PRAGMA foreign_keys = ON')
|
||||
return db
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ids(player_ids):
|
||||
seen = set()
|
||||
result = []
|
||||
for value in player_ids or []:
|
||||
steam_id = str(value).strip()
|
||||
if steam_id and steam_id not in seen:
|
||||
seen.add(steam_id)
|
||||
result.append(steam_id)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def ensure_initial_version(web_db_path=None, l2_db_path=None):
|
||||
web = RosterVersionService._connect_web(web_db_path)
|
||||
try:
|
||||
current = web.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM team_roster_versions
|
||||
WHERE is_current = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
).fetchone()
|
||||
if current:
|
||||
return int(current['id'])
|
||||
|
||||
lineup = web.execute(
|
||||
"""
|
||||
SELECT player_ids_json
|
||||
FROM team_lineups
|
||||
WHERE is_active = 1
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
).fetchone()
|
||||
if not lineup:
|
||||
return None
|
||||
try:
|
||||
player_ids = RosterVersionService._normalize_ids(
|
||||
json.loads(lineup['player_ids_json'] or '[]')
|
||||
)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
if not player_ids:
|
||||
return None
|
||||
|
||||
effective_from = int(time.time())
|
||||
l2_path = l2_db_path or Config.DB_L2_PATH
|
||||
l2 = sqlite3.connect(l2_path)
|
||||
try:
|
||||
placeholders = ','.join('?' for _ in player_ids)
|
||||
row = l2.execute(
|
||||
f"""
|
||||
SELECT MIN(m.start_time)
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON m.match_id = p.match_id
|
||||
WHERE p.steam_id_64 IN ({placeholders})
|
||||
""",
|
||||
player_ids,
|
||||
).fetchone()
|
||||
if row and row[0]:
|
||||
effective_from = int(row[0])
|
||||
finally:
|
||||
l2.close()
|
||||
|
||||
cursor = web.execute(
|
||||
"""
|
||||
INSERT INTO team_roster_versions (
|
||||
name, effective_from, is_current, notes
|
||||
) VALUES (?, ?, 1, ?)
|
||||
""",
|
||||
['2.0.0 Beta Initial Roster', effective_from, 'Bootstrapped from active lineup'],
|
||||
)
|
||||
version_id = int(cursor.lastrowid)
|
||||
web.executemany(
|
||||
"""
|
||||
INSERT INTO team_roster_members (
|
||||
roster_version_id, steam_id_64, member_role, position_order
|
||||
) VALUES (?, ?, 'member', ?)
|
||||
""",
|
||||
[
|
||||
(version_id, steam_id, index)
|
||||
for index, steam_id in enumerate(player_ids)
|
||||
],
|
||||
)
|
||||
web.commit()
|
||||
return version_id
|
||||
finally:
|
||||
web.close()
|
||||
|
||||
@staticmethod
|
||||
def snapshot_roster(
|
||||
player_ids,
|
||||
name=None,
|
||||
effective_from=None,
|
||||
roles=None,
|
||||
web_db_path=None,
|
||||
):
|
||||
normalized_ids = RosterVersionService._normalize_ids(player_ids)
|
||||
if not normalized_ids:
|
||||
raise ValueError('Roster version requires at least one player')
|
||||
|
||||
roles = roles or {}
|
||||
effective_from = int(effective_from or time.time())
|
||||
web = RosterVersionService._connect_web(web_db_path)
|
||||
try:
|
||||
current = web.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM team_roster_versions
|
||||
WHERE is_current = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
).fetchone()
|
||||
if current:
|
||||
current_ids = [
|
||||
str(row[0]) for row in web.execute(
|
||||
"""
|
||||
SELECT steam_id_64
|
||||
FROM team_roster_members
|
||||
WHERE roster_version_id = ?
|
||||
ORDER BY position_order, steam_id_64
|
||||
""",
|
||||
[current['id']],
|
||||
)
|
||||
]
|
||||
if current_ids == normalized_ids:
|
||||
return int(current['id']), False
|
||||
web.execute(
|
||||
"""
|
||||
UPDATE team_roster_versions
|
||||
SET is_current = 0, effective_to = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
[effective_from - 1, current['id']],
|
||||
)
|
||||
|
||||
cursor = web.execute(
|
||||
"""
|
||||
INSERT INTO team_roster_versions (
|
||||
name, effective_from, is_current
|
||||
) VALUES (?, ?, 1)
|
||||
""",
|
||||
[
|
||||
name or f'Roster {time.strftime("%Y-%m-%d")}',
|
||||
effective_from,
|
||||
],
|
||||
)
|
||||
version_id = int(cursor.lastrowid)
|
||||
values = []
|
||||
for index, steam_id in enumerate(normalized_ids):
|
||||
role = roles.get(steam_id, 'member')
|
||||
if role not in {'starter', 'substitute', 'member'}:
|
||||
role = 'member'
|
||||
values.append((version_id, steam_id, role, index))
|
||||
web.executemany(
|
||||
"""
|
||||
INSERT INTO team_roster_members (
|
||||
roster_version_id, steam_id_64, member_role, position_order
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
values,
|
||||
)
|
||||
web.commit()
|
||||
return version_id, True
|
||||
except Exception:
|
||||
web.rollback()
|
||||
raise
|
||||
finally:
|
||||
web.close()
|
||||
|
||||
@staticmethod
|
||||
def list_versions(web_db_path=None):
|
||||
web = RosterVersionService._connect_web(web_db_path)
|
||||
try:
|
||||
rows = web.execute(
|
||||
"""
|
||||
SELECT
|
||||
version.*,
|
||||
COUNT(member.steam_id_64) AS member_count
|
||||
FROM team_roster_versions version
|
||||
LEFT JOIN team_roster_members member
|
||||
ON member.roster_version_id = version.id
|
||||
GROUP BY version.id
|
||||
ORDER BY version.effective_from DESC, version.id DESC
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
finally:
|
||||
web.close()
|
||||
|
||||
@staticmethod
|
||||
def get_current_members(web_db_path=None):
|
||||
web = RosterVersionService._connect_web(web_db_path)
|
||||
try:
|
||||
rows = web.execute(
|
||||
"""
|
||||
SELECT member.*, version.name AS version_name
|
||||
FROM team_roster_members member
|
||||
JOIN team_roster_versions version
|
||||
ON version.id = member.roster_version_id
|
||||
WHERE version.is_current = 1
|
||||
ORDER BY member.position_order, member.steam_id_64
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
finally:
|
||||
web.close()
|
||||
|
||||
@staticmethod
|
||||
def update_current_member_role(steam_id, member_role, web_db_path=None):
|
||||
if member_role not in {'starter', 'substitute', 'member'}:
|
||||
raise ValueError('Invalid roster member role')
|
||||
web = RosterVersionService._connect_web(web_db_path)
|
||||
try:
|
||||
cursor = web.execute(
|
||||
"""
|
||||
UPDATE team_roster_members
|
||||
SET member_role = ?
|
||||
WHERE steam_id_64 = ?
|
||||
AND roster_version_id = (
|
||||
SELECT id
|
||||
FROM team_roster_versions
|
||||
WHERE is_current = 1
|
||||
LIMIT 1
|
||||
)
|
||||
""",
|
||||
[member_role, str(steam_id)],
|
||||
)
|
||||
if cursor.rowcount != 1:
|
||||
raise ValueError('Player is not in the current roster version')
|
||||
web.commit()
|
||||
finally:
|
||||
web.close()
|
||||
@@ -0,0 +1,173 @@
|
||||
import json
|
||||
|
||||
from web.database import query_db
|
||||
from web.services.roster_version_service import RosterVersionService
|
||||
from web.services.team_context_service import TeamContextService
|
||||
|
||||
|
||||
class TeamPerformanceService:
|
||||
@staticmethod
|
||||
def _player_identity_map(steam_ids):
|
||||
if not steam_ids:
|
||||
return {}
|
||||
placeholders = ','.join('?' for _ in steam_ids)
|
||||
rows = query_db(
|
||||
'l2',
|
||||
f"""
|
||||
SELECT steam_id_64, username, avatar_url
|
||||
FROM dim_players
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
""",
|
||||
steam_ids,
|
||||
)
|
||||
return {str(row['steam_id_64']): dict(row) for row in rows}
|
||||
|
||||
@staticmethod
|
||||
def get_team_summary():
|
||||
roster_ids = TeamContextService.get_active_roster_ids()
|
||||
if not roster_ids:
|
||||
return {
|
||||
'matches': 0,
|
||||
'wins': 0,
|
||||
'win_rate': 0,
|
||||
'avg_rating': 0,
|
||||
'maps': [],
|
||||
}
|
||||
placeholders = ','.join('?' for _ in roster_ids)
|
||||
rows = query_db(
|
||||
'l2',
|
||||
f"""
|
||||
SELECT
|
||||
p.match_id,
|
||||
CASE
|
||||
WHEN p.group_id IN (1, 2) THEN p.group_id
|
||||
WHEN p.team_id IN (1, 2) THEN p.team_id
|
||||
END AS team_key,
|
||||
COUNT(DISTINCT p.steam_id_64) AS roster_count,
|
||||
AVG(p.rating) AS avg_rating,
|
||||
MAX(p.is_win) AS is_win,
|
||||
m.map_name,
|
||||
m.start_time
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON m.match_id = p.match_id
|
||||
WHERE p.steam_id_64 IN ({placeholders})
|
||||
GROUP BY p.match_id, team_key
|
||||
HAVING roster_count >= 2 AND team_key IS NOT NULL
|
||||
ORDER BY m.start_time DESC
|
||||
""",
|
||||
roster_ids,
|
||||
)
|
||||
matches = len(rows)
|
||||
wins = sum(int(row['is_win'] or 0) for row in rows)
|
||||
map_stats = {}
|
||||
for row in rows:
|
||||
item = map_stats.setdefault(
|
||||
row['map_name'] or 'Unknown',
|
||||
{'map_name': row['map_name'] or 'Unknown', 'matches': 0, 'wins': 0},
|
||||
)
|
||||
item['matches'] += 1
|
||||
item['wins'] += int(row['is_win'] or 0)
|
||||
maps = []
|
||||
for item in map_stats.values():
|
||||
item['win_rate'] = item['wins'] / item['matches']
|
||||
maps.append(item)
|
||||
maps.sort(key=lambda item: item['matches'], reverse=True)
|
||||
return {
|
||||
'matches': matches,
|
||||
'wins': wins,
|
||||
'losses': matches - wins,
|
||||
'win_rate': wins / matches if matches else 0,
|
||||
'avg_rating': (
|
||||
sum(float(row['avg_rating'] or 0) for row in rows) / matches
|
||||
if matches else 0
|
||||
),
|
||||
'maps': maps,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_current_roster():
|
||||
members = RosterVersionService.get_current_members()
|
||||
identities = TeamPerformanceService._player_identity_map(
|
||||
[member['steam_id_64'] for member in members]
|
||||
)
|
||||
result = []
|
||||
for member in members:
|
||||
item = dict(member)
|
||||
item.update(identities.get(str(member['steam_id_64']), {}))
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_top_duos(limit=12):
|
||||
rows = query_db(
|
||||
'l3',
|
||||
"""
|
||||
SELECT *
|
||||
FROM dm_duo_stats
|
||||
ORDER BY sample_reliable DESC, matches DESC,
|
||||
win_rate DESC, avg_combined_rating DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
[limit],
|
||||
)
|
||||
steam_ids = {
|
||||
str(value)
|
||||
for row in rows
|
||||
for value in (row['steam_id_a'], row['steam_id_b'])
|
||||
}
|
||||
identities = TeamPerformanceService._player_identity_map(sorted(steam_ids))
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['player_a'] = identities.get(str(row['steam_id_a']), {})
|
||||
item['player_b'] = identities.get(str(row['steam_id_b']), {})
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_top_lineups(player_count=None, limit=12):
|
||||
args = []
|
||||
where = ''
|
||||
if player_count:
|
||||
where = 'WHERE player_count = ?'
|
||||
args.append(int(player_count))
|
||||
args.append(limit)
|
||||
rows = query_db(
|
||||
'l3',
|
||||
f"""
|
||||
SELECT *
|
||||
FROM dm_lineup_stats
|
||||
{where}
|
||||
ORDER BY sample_reliable DESC, matches DESC,
|
||||
win_rate DESC, avg_team_rating DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
args,
|
||||
)
|
||||
all_ids = set()
|
||||
parsed = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
item['player_ids'] = [
|
||||
str(value) for value in json.loads(item['player_ids_json'])
|
||||
]
|
||||
all_ids.update(item['player_ids'])
|
||||
parsed.append(item)
|
||||
identities = TeamPerformanceService._player_identity_map(sorted(all_ids))
|
||||
for item in parsed:
|
||||
item['players'] = [
|
||||
identities.get(steam_id, {'username': steam_id})
|
||||
for steam_id in item['player_ids']
|
||||
]
|
||||
return parsed
|
||||
|
||||
@staticmethod
|
||||
def get_dashboard():
|
||||
return {
|
||||
'summary': TeamPerformanceService.get_team_summary(),
|
||||
'roster': TeamPerformanceService.get_current_roster(),
|
||||
'versions': RosterVersionService.list_versions(),
|
||||
'duos': TeamPerformanceService.get_top_duos(),
|
||||
'lineups': TeamPerformanceService.get_top_lineups(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user