feat: implement SQLite database manager for user data, points, and Supabase bindings.

This commit is contained in:
2569718930@qq.com
2026-03-13 19:48:40 +08:00
parent cd0a5d519f
commit 786765e48b
+116 -19
View File
@@ -86,12 +86,40 @@ class DBManager:
PRIMARY KEY (week_key, telegram_id) PRIMARY KEY (week_key, telegram_id)
) )
""") """)
conn.execute("""
CREATE TABLE IF NOT EXISTS supabase_bindings (
supabase_user_id TEXT PRIMARY KEY,
telegram_id INTEGER NOT NULL,
supabase_email TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_supabase_bindings_telegram_id ON supabase_bindings(telegram_id)"
)
self._ensure_column(conn, "users", "daily_points", "INTEGER DEFAULT 0") self._ensure_column(conn, "users", "daily_points", "INTEGER DEFAULT 0")
self._ensure_column(conn, "users", "daily_points_date", "TEXT") self._ensure_column(conn, "users", "daily_points_date", "TEXT")
self._ensure_column(conn, "users", "weekly_points", "INTEGER DEFAULT 0") self._ensure_column(conn, "users", "weekly_points", "INTEGER DEFAULT 0")
self._ensure_column(conn, "users", "weekly_points_week", "TEXT") self._ensure_column(conn, "users", "weekly_points_week", "TEXT")
self._ensure_column(conn, "users", "supabase_user_id", "TEXT") self._ensure_column(conn, "users", "supabase_user_id", "TEXT")
self._ensure_column(conn, "users", "supabase_email", "TEXT") self._ensure_column(conn, "users", "supabase_email", "TEXT")
# Migrate legacy one-to-one binding column into mapping table.
conn.execute(
"""
INSERT OR IGNORE INTO supabase_bindings (
supabase_user_id, telegram_id, supabase_email, created_at, updated_at
)
SELECT
lower(trim(COALESCE(supabase_user_id, ''))),
telegram_id,
COALESCE(supabase_email, ''),
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM users
WHERE trim(COALESCE(supabase_user_id, '')) <> ''
"""
)
conn.commit() conn.commit()
logger.info(f"Database initialized successfully path={self.db_path}") logger.info(f"Database initialized successfully path={self.db_path}")
@@ -132,6 +160,47 @@ class DBManager:
if column not in existing: if column not in existing:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {ddl}") conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {ddl}")
def _find_telegram_id_by_supabase_user_id(
self,
conn: sqlite3.Connection,
supabase_user_id: str,
) -> Optional[int]:
key = str(supabase_user_id or "").strip().lower()
if not key:
return None
conn.row_factory = sqlite3.Row
row = conn.execute(
"""
SELECT telegram_id
FROM supabase_bindings
WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ?
LIMIT 1
""",
(key,),
).fetchone()
if row:
try:
return int(row["telegram_id"])
except Exception:
return None
# Legacy fallback before supabase_bindings migration.
row = conn.execute(
"""
SELECT telegram_id
FROM users
WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ?
LIMIT 1
""",
(key,),
).fetchone()
if not row:
return None
try:
return int(row["telegram_id"])
except Exception:
return None
def get_user(self, telegram_id: int) -> Optional[Dict[str, Any]]: def get_user(self, telegram_id: int) -> Optional[Dict[str, Any]]:
with self._get_connection() as conn: with self._get_connection() as conn:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
@@ -157,14 +226,17 @@ class DBManager:
return None return None
with self._get_connection() as conn: with self._get_connection() as conn:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
telegram_id = self._find_telegram_id_by_supabase_user_id(conn, key)
if telegram_id is None:
return None
row = conn.execute( row = conn.execute(
""" """
SELECT * SELECT *
FROM users FROM users
WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ? WHERE telegram_id = ?
LIMIT 1 LIMIT 1
""", """,
(key,), (int(telegram_id),),
).fetchone() ).fetchone()
if row: if row:
return dict(row) return dict(row)
@@ -196,12 +268,11 @@ class DBManager:
supabase_email: str = "", supabase_email: str = "",
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Strict one-to-one binding between Telegram account and Supabase account. Bind Supabase account to Telegram account.
Rules: Rules:
- One telegram_id can only bind one supabase_user_id (no overwrite via /bind). - One supabase_user_id can only belong to one telegram_id.
- One supabase_user_id can only bind one telegram_id. - One telegram_id can bind multiple supabase_user_id (shared points/profile).
- Rebind requires explicit unbind first.
""" """
normalized_uid = str(supabase_user_id or "").strip().lower() normalized_uid = str(supabase_user_id or "").strip().lower()
normalized_email = str(supabase_email or "").strip() normalized_email = str(supabase_email or "").strip()
@@ -237,7 +308,7 @@ class DBManager:
owner_row = conn.execute( owner_row = conn.execute(
""" """
SELECT telegram_id SELECT telegram_id
FROM users FROM supabase_bindings
WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ? WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ?
LIMIT 1 LIMIT 1
""", """,
@@ -245,13 +316,6 @@ class DBManager:
).fetchone() ).fetchone()
owner_telegram_id = int(owner_row["telegram_id"]) if owner_row else None owner_telegram_id = int(owner_row["telegram_id"]) if owner_row else None
if current_uid and current_uid != normalized_uid:
return {
"ok": False,
"reason": "telegram_already_bound_other",
"current_supabase_user_id": current_uid,
}
if owner_telegram_id is not None and owner_telegram_id != int(telegram_id): if owner_telegram_id is not None and owner_telegram_id != int(telegram_id):
return { return {
"ok": False, "ok": False,
@@ -259,6 +323,18 @@ class DBManager:
"owner_telegram_id": owner_telegram_id, "owner_telegram_id": owner_telegram_id,
} }
conn.execute(
"""
INSERT INTO supabase_bindings (supabase_user_id, telegram_id, supabase_email, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(supabase_user_id) DO UPDATE SET
telegram_id = excluded.telegram_id,
supabase_email = excluded.supabase_email,
updated_at = excluded.updated_at
""",
(normalized_uid, int(telegram_id), normalized_email, datetime.now().isoformat()),
)
if current_uid == normalized_uid: if current_uid == normalized_uid:
# Keep idempotent bind behavior while allowing email refresh. # Keep idempotent bind behavior while allowing email refresh.
conn.execute( conn.execute(
@@ -296,9 +372,25 @@ class DBManager:
(telegram_id,), (telegram_id,),
).fetchone() ).fetchone()
current_uid = str((current["supabase_user_id"] if current else "") or "").strip() current_uid = str((current["supabase_user_id"] if current else "") or "").strip()
if not current_uid: links = conn.execute(
"""
SELECT supabase_user_id
FROM supabase_bindings
WHERE telegram_id = ?
LIMIT 1
""",
(int(telegram_id),),
).fetchone()
if not current_uid and not links:
return {"ok": True, "reason": "not_bound"} return {"ok": True, "reason": "not_bound"}
conn.execute(
"""
DELETE FROM supabase_bindings
WHERE telegram_id = ?
""",
(int(telegram_id),),
)
conn.execute( conn.execute(
""" """
UPDATE users UPDATE users
@@ -505,14 +597,17 @@ class DBManager:
with self._get_connection() as conn: with self._get_connection() as conn:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
telegram_id = self._find_telegram_id_by_supabase_user_id(conn, key)
if telegram_id is None:
return {"ok": False, "reason": "user_missing", "balance": 0, "required": amount}
row = conn.execute( row = conn.execute(
""" """
SELECT telegram_id, points SELECT telegram_id, points
FROM users FROM users
WHERE lower(trim(COALESCE(supabase_user_id, ''))) = ? WHERE telegram_id = ?
LIMIT 1 LIMIT 1
""", """,
(key,), (int(telegram_id),),
).fetchone() ).fetchone()
if not row: if not row:
return {"ok": False, "reason": "user_missing", "balance": 0, "required": amount} return {"ok": False, "reason": "user_missing", "balance": 0, "required": amount}
@@ -588,11 +683,13 @@ class DBManager:
week_key = f"{iso_year}-W{iso_week:02d}" week_key = f"{iso_year}-W{iso_week:02d}"
with self._get_connection() as conn: with self._get_connection() as conn:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
target_telegram_id = self._find_telegram_id_by_supabase_user_id(conn, key)
if target_telegram_id is None:
return {"weekly_points": 0, "weekly_rank": None, "total_ranked": 0}
rows = conn.execute( rows = conn.execute(
""" """
SELECT SELECT
telegram_id, telegram_id,
lower(trim(COALESCE(supabase_user_id, ''))) AS supabase_key,
COALESCE(points, 0) AS points, COALESCE(points, 0) AS points,
COALESCE(message_count, 0) AS message_count, COALESCE(message_count, 0) AS message_count,
CASE CASE
@@ -608,7 +705,7 @@ class DBManager:
weekly_rank: Optional[int] = None weekly_rank: Optional[int] = None
weekly_points = 0 weekly_points = 0
for idx, row in enumerate(rows, start=1): for idx, row in enumerate(rows, start=1):
if str(row["supabase_key"] or "") == key: if int(row["telegram_id"] or 0) == int(target_telegram_id):
weekly_rank = idx weekly_rank = idx
weekly_points = int(row["weekly_points"] or 0) weekly_points = int(row["weekly_points"] or 0)
break break