230 lines
9.1 KiB
Python
230 lines
9.1 KiB
Python
"""
|
|
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()
|