""" Olympus 数据库操作模块 用法: python olympus_db.py # 建表 + 写入饰品桶种子数据 连接参数可用环境变量覆盖: OLY_DB_HOST / OLY_DB_PORT / OLY_DB_USER / OLY_DB_PASSWORD / OLY_DB_NAME 默认对齐本地开发库 (127.0.0.1:23306 changzhou,见 docker/compose/docker-compose.yml)。 """ import json import os import time from contextlib import contextmanager from pathlib import Path import pymysql from olympus.config import ORNAMENTS DB_CONFIG = { 'host': os.environ.get('OLY_DB_HOST', '127.0.0.1'), 'port': int(os.environ.get('OLY_DB_PORT', '23306')), 'user': os.environ.get('OLY_DB_USER', 'changzhou'), 'password': os.environ.get('OLY_DB_PASSWORD', 'iTSJSPPZM3LSGAPC'), 'database': os.environ.get('OLY_DB_NAME', 'changzhou'), 'charset': 'utf8mb4', 'local_infile': True, } SCHEMA_FILE = Path(__file__).resolve().parent / 'schema.sql' @contextmanager def get_conn(): conn = pymysql.connect(**DB_CONFIG) try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() # ============================================================ # 建表(执行 schema.sql 中所有语句,注释行自动跳过) # ============================================================ def _load_schema_statements(): text = SCHEMA_FILE.read_text(encoding='utf-8') lines = [ln for ln in text.splitlines() if not ln.strip().startswith('--')] statements = [s.strip() for s in '\n'.join(lines).split(';') if s.strip()] return statements def create_tables(): """建表 + 写入饰品桶种子数据(幂等,可重复执行)""" stmts = _load_schema_statements() with get_conn() as conn: cur = conn.cursor() for sql in stmts: cur.execute(sql) print(f'OK: executed {len(stmts)} statements (tables + ornament seed).') def verify_tables(): """校验所有 olympus 表已存在并输出行数""" with get_conn() as conn: cur = conn.cursor() cur.execute("SHOW TABLES LIKE 'game_olympus_%'") tables = [row[0] for row in cur.fetchall()] for t in sorted(tables): cur.execute(f"SELECT COUNT(*) FROM {t}") print(f'{t}: {cur.fetchone()[0]} rows') return tables # ============================================================ # Ornament 操作 # ============================================================ def insert_ornaments(): """按 config.ORNAMENTS 覆盖写入饰品桶(与 schema.sql 种子一致)""" with get_conn() as conn: cur = conn.cursor() for o in ORNAMENTS: cur.execute(""" INSERT INTO game_olympus_ornament (id, game_type, expect_mul, name, range_low, range_high) VALUES (%s,'OLYMPUS',%s,%s,%s,%s) ON DUPLICATE KEY UPDATE expect_mul=VALUES(expect_mul), name=VALUES(name), range_low=VALUES(range_low), range_high=VALUES(range_high) """, (o['id'], o['expect_mul'], o['name'], o['range_low'], o['range_high'])) # ============================================================ # Box 操作 # ============================================================ def insert_box(box): with get_conn() as conn: cur = conn.cursor() cur.execute(""" INSERT INTO game_olympus_box (box_id, game_type, box_type, bet, name, price, total_ornaments, rtp, sort, status) VALUES (%s,'OLYMPUS',%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE box_type=VALUES(box_type), bet=VALUES(bet), name=VALUES(name), price=VALUES(price), total_ornaments=VALUES(total_ornaments), rtp=VALUES(rtp) """, (box['box_id'], box['box_type'], box['bet'], box['name'], box['price'], box['total_ornaments'], box['rtp'], box.get('sort', 0), box.get('status', '1'))) def query_boxes(): with get_conn() as conn: cur = conn.cursor(pymysql.cursors.DictCursor) cur.execute("SELECT * FROM game_olympus_box WHERE status='1' ORDER BY box_id") return cur.fetchall() # ============================================================ # Round / Step 批量写入(单连接清空+写入,死锁自动重试) # ============================================================ def get_max_round_id(): with get_conn() as conn: cur = conn.cursor() cur.execute("SELECT COALESCE(MAX(round_id), 0) FROM game_olympus_round") return cur.fetchone()[0] def get_max_step_id(): with get_conn() as conn: cur = conn.cursor() cur.execute("SELECT COALESCE(MAX(id), 0) FROM game_olympus_step") return cur.fetchone()[0] def _j(v, default='[]'): if v is None: return default return v if isinstance(v, str) else json.dumps(v, ensure_ascii=False) def write_box_distribution(box_id, counts, audience='common'): """单连接:按身份清空并写入饰品桶分布(只动该 audience,不影响其他身份)""" for attempt in range(5): try: with get_conn() as conn: cur = conn.cursor() cur.execute( "DELETE FROM game_olympus_box_ornament WHERE box_id=%s AND audience=%s", (box_id, audience)) if counts: sql = ("INSERT INTO game_olympus_box_ornament" " (box_id, ornament_id, audience, count, used)" " VALUES (%s,%s,%s,%s,0)") rows = [(box_id, oid, audience, cnt) for oid, cnt in sorted(counts.items()) if cnt > 0] cur.executemany(sql, rows) return except Exception as e: if 'Deadlock' in str(e) and attempt < 4: time.sleep(0.5 * (attempt + 1)) continue raise def write_box_distributions(box_id, counts_by_audience): """一次写入多套身份分布。counts_by_audience: {audience: {ornament_id: count}}""" for audience, counts in counts_by_audience.items(): write_box_distribution(box_id, counts, audience) def write_rounds_and_steps(box_id, rounds_data, steps_data): """单连接:清空旧回合+步骤,批量写入新数据""" for attempt in range(5): try: with get_conn() as conn: cur = conn.cursor() cur.execute("""DELETE FROM game_olympus_step WHERE round_id IN (SELECT round_id FROM game_olympus_round WHERE box_id=%s)""", (box_id,)) cur.execute("DELETE FROM game_olympus_round WHERE box_id=%s", (box_id,)) if rounds_data: sql_r = """INSERT INTO game_olympus_round (round_id, box_id, ornament_id, actual_mul, free_spin_count, total_cascades) VALUES (%s,%s,%s,%s,%s,%s)""" rows_r = [(rd['round_id'], rd['box_id'], rd['ornament_id'], rd['actual_mul'], rd['free_spin_count'], rd['total_cascades']) for rd in rounds_data] cur.executemany(sql_r, rows_r) if steps_data: sql_s = """INSERT INTO game_olympus_step (id, round_id, free_spin_id, cascade_seq, grid_state, multiplier_detail, symbol_links, accumulated_symbols, spin_winners, removed_positions, drop_data, fill_data, scatter_trigger, retrigger_info, base_score, win_amount, accumulator_before, accumulator_after, spin_win, win_capped, bigwin_level, is_final) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" rows_s = [(s['id'], s['round_id'], s['free_spin_id'], s['cascade_seq'], _j(s['grid_state']), _j(s.get('multiplier_detail')), _j(s.get('symbol_links')), _j(s.get('accumulated_symbols'), default='{}'), _j(s.get('spin_winners')), _j(s.get('removed_positions')), _j(s.get('drop_data')), _j(s.get('fill_data')), _j(s.get('scatter_trigger'), default=None), _j(s.get('retrigger_info'), default=None), s['base_score'], s['win_amount'], s['accumulator_before'], s['accumulator_after'], s['spin_win'], s.get('win_capped', 0), s.get('bigwin_level'), s['is_final']) for s in steps_data] cur.executemany(sql_s, rows_s) return except Exception as e: if 'Deadlock' in str(e) and attempt < 4: time.sleep(0.5 * (attempt + 1)) continue raise if __name__ == '__main__': create_tables() verify_tables()