208 lines
6.4 KiB
Python
208 lines
6.4 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""
|
|||
|
|
数据库层:SQLite 统一存储结构化数据。
|
|||
|
|
Schema 采用"运动-联赛-球队-球员-比赛"分层设计,天然支持扩展:
|
|||
|
|
- 新增 CBA → sports 表加一行 basketball,leagues 表加 CBA(复用全部代码)
|
|||
|
|
- 新增足球 → sports 表加 football,league 可加 英超/西甲,球员字段可扩展
|
|||
|
|
- 新增人物角色 → persons.role 自由扩展(coach/agent/commentator/host/gm...)
|
|||
|
|
"""
|
|||
|
|
import sqlite3
|
|||
|
|
import threading
|
|||
|
|
|
|||
|
|
from config import DB_PATH
|
|||
|
|
|
|||
|
|
_SCHEMA = """
|
|||
|
|
CREATE TABLE IF NOT EXISTS sports (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
code TEXT UNIQUE NOT NULL, -- 'basketball' / 'football' ...
|
|||
|
|
name TEXT NOT NULL, -- '篮球' / '足球'
|
|||
|
|
name_en TEXT,
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS leagues (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
sport_id INTEGER NOT NULL REFERENCES sports(id),
|
|||
|
|
code TEXT UNIQUE NOT NULL, -- 'NBA' / 'CBA' / 'EPL'
|
|||
|
|
name TEXT NOT NULL, -- '美国职业篮球联赛'
|
|||
|
|
name_en TEXT,
|
|||
|
|
country TEXT,
|
|||
|
|
season TEXT, -- '2025-26'
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS teams (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
league_id INTEGER NOT NULL REFERENCES leagues(id),
|
|||
|
|
code TEXT, -- 'LAL'
|
|||
|
|
name TEXT NOT NULL, -- '洛杉矶湖人'
|
|||
|
|
name_en TEXT, -- 'Los Angeles Lakers'
|
|||
|
|
city TEXT,
|
|||
|
|
arena TEXT,
|
|||
|
|
founded INTEGER,
|
|||
|
|
champion_count INTEGER,
|
|||
|
|
head_coach TEXT,
|
|||
|
|
logo_url TEXT,
|
|||
|
|
intro TEXT,
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS players (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
team_id INTEGER REFERENCES teams(id),
|
|||
|
|
name TEXT NOT NULL,
|
|||
|
|
name_en TEXT,
|
|||
|
|
position TEXT, -- PG/SG/SF/PF/C
|
|||
|
|
number INTEGER,
|
|||
|
|
height_cm INTEGER,
|
|||
|
|
weight_kg INTEGER,
|
|||
|
|
birth_date TEXT,
|
|||
|
|
country TEXT,
|
|||
|
|
draft_year INTEGER,
|
|||
|
|
draft_pick INTEGER, -- 0 = 落选
|
|||
|
|
salary_m REAL, -- 年薪(万美元)
|
|||
|
|
season_pts REAL, season_reb REAL, season_ast REAL,
|
|||
|
|
season_stl REAL, season_blk REAL, season_min REAL, -- 本赛季场均
|
|||
|
|
career_pts REAL, career_reb REAL, career_ast REAL, -- 生涯场均
|
|||
|
|
career_games INTEGER,
|
|||
|
|
career_steals INTEGER, career_blocks INTEGER, -- 生涯总数
|
|||
|
|
awards TEXT, -- 主要荣誉
|
|||
|
|
bio TEXT,
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS games (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
league_id INTEGER NOT NULL REFERENCES leagues(id),
|
|||
|
|
season TEXT,
|
|||
|
|
round_name TEXT, -- 常规赛/季后赛首轮/总决赛...
|
|||
|
|
game_time TEXT, -- '2026-06-05 09:00'
|
|||
|
|
status TEXT, -- scheduled / finished / live
|
|||
|
|
home_team_id INTEGER REFERENCES teams(id),
|
|||
|
|
away_team_id INTEGER REFERENCES teams(id),
|
|||
|
|
home_score INTEGER,
|
|||
|
|
away_score INTEGER,
|
|||
|
|
venue TEXT,
|
|||
|
|
broadcast TEXT,
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS game_player_stats (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
game_id INTEGER NOT NULL REFERENCES games(id),
|
|||
|
|
player_id INTEGER NOT NULL REFERENCES players(id),
|
|||
|
|
team_id INTEGER NOT NULL REFERENCES teams(id),
|
|||
|
|
points INTEGER DEFAULT 0, rebounds INTEGER DEFAULT 0,
|
|||
|
|
assists INTEGER DEFAULT 0, steals INTEGER DEFAULT 0,
|
|||
|
|
blocks INTEGER DEFAULT 0, minutes INTEGER DEFAULT 0
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS standings (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
league_id INTEGER NOT NULL REFERENCES leagues(id),
|
|||
|
|
season TEXT,
|
|||
|
|
team_id INTEGER NOT NULL REFERENCES teams(id),
|
|||
|
|
wins INTEGER DEFAULT 0, losses INTEGER DEFAULT 0,
|
|||
|
|
win_pct REAL DEFAULT 0,
|
|||
|
|
conference TEXT, -- 西部/东部
|
|||
|
|
rank INTEGER
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS news (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
sport_id INTEGER NOT NULL REFERENCES sports(id),
|
|||
|
|
league_id INTEGER REFERENCES leagues(id),
|
|||
|
|
team_id INTEGER REFERENCES teams(id),
|
|||
|
|
title TEXT NOT NULL,
|
|||
|
|
content TEXT NOT NULL,
|
|||
|
|
author TEXT,
|
|||
|
|
source TEXT,
|
|||
|
|
publish_time TEXT,
|
|||
|
|
tags TEXT,
|
|||
|
|
kind TEXT DEFAULT 'news', -- news / wiki(百科词条)
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS persons (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
sport_id INTEGER NOT NULL REFERENCES sports(id),
|
|||
|
|
league_id INTEGER REFERENCES leagues(id),
|
|||
|
|
team_id INTEGER REFERENCES teams(id),
|
|||
|
|
name TEXT NOT NULL,
|
|||
|
|
name_en TEXT,
|
|||
|
|
role TEXT, -- coach/agent/commentator/host/gm/legend...
|
|||
|
|
role_cn TEXT, -- 教练/经纪人/评论员/主持人/总经理...
|
|||
|
|
title TEXT,
|
|||
|
|
bio TEXT,
|
|||
|
|
achievements TEXT,
|
|||
|
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_players_team ON players(team_id);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_players_name ON players(name);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_games_time ON games(game_time);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_games_team ON games(home_team_id);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_news_time ON news(publish_time);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_persons_role ON persons(role);
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
_lock = threading.Lock()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_conn():
|
|||
|
|
conn = sqlite3.connect(DB_PATH, timeout=30)
|
|||
|
|
conn.row_factory = sqlite3.Row
|
|||
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|||
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|||
|
|
return conn
|
|||
|
|
|
|||
|
|
|
|||
|
|
def init_db():
|
|||
|
|
with _lock:
|
|||
|
|
conn = get_conn()
|
|||
|
|
try:
|
|||
|
|
conn.executescript(_SCHEMA)
|
|||
|
|
conn.commit()
|
|||
|
|
finally:
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def query(sql, args=()):
|
|||
|
|
conn = get_conn()
|
|||
|
|
try:
|
|||
|
|
cur = conn.execute(sql, args)
|
|||
|
|
return [dict(r) for r in cur.fetchall()]
|
|||
|
|
finally:
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def query_one(sql, args=()):
|
|||
|
|
rows = query(sql, args)
|
|||
|
|
return rows[0] if rows else None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def execute(sql, args=()):
|
|||
|
|
conn = get_conn()
|
|||
|
|
try:
|
|||
|
|
cur = conn.execute(sql, args)
|
|||
|
|
conn.commit()
|
|||
|
|
return cur.lastrowid
|
|||
|
|
finally:
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def executemany(sql, rows):
|
|||
|
|
conn = get_conn()
|
|||
|
|
try:
|
|||
|
|
conn.executemany(sql, rows)
|
|||
|
|
conn.commit()
|
|||
|
|
finally:
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def table_count(table):
|
|||
|
|
return query_one(f"SELECT COUNT(*) AS c FROM {table}")["c"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fuzzy(expr): # SQL LIKE 模糊匹配(转义 % _)
|
|||
|
|
return expr.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|