1
0
Fork 0
forked from zovos/bot_tg
bot_tg/games/economy.py
q add3553ce0 feat: переход с SQLite на PostgreSQL + скрипт миграции
- Все 4 SQLite базы объединены в одну PostgreSQL
- Новый db.py с ThreadedConnectionPool (psycopg2)
- docker-compose: сервис postgres с healthcheck, данные в ./db/postgres
- scripts/migrate_sqlite_to_pg.py — миграция существующих данных
- Убраны мёртвый код и дублирующий import в main.py и talk_handler.py
- .env удалён из git-индекса

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 23:25:22 +03:00

669 lines
28 KiB
Python

import logging
import random
from datetime import datetime, timedelta
from typing import Dict
import psycopg2.extras
from db import get_conn
logger = logging.getLogger(__name__)
class EconomyManager:
def __init__(self):
self.init_db()
def init_db(self):
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS user_balances (
user_id BIGINT PRIMARY KEY,
balance REAL DEFAULT 0.0,
daily_income REAL DEFAULT 0.0,
last_daily_reset DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS deposits (
id SERIAL PRIMARY KEY,
user_id BIGINT,
amount REAL,
interest_rate REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
matures_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS loans (
id SERIAL PRIMARY KEY,
user_id BIGINT,
amount REAL,
interest_rate REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
due_at TIMESTAMP,
is_repaid BOOLEAN DEFAULT FALSE
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS transactions (
id SERIAL PRIMARY KEY,
from_user_id BIGINT,
to_user_id BIGINT,
amount REAL,
transaction_type TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS taxes (
id SERIAL PRIMARY KEY,
user_id BIGINT,
amount REAL,
tax_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS economy_settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS daily_activity (
id SERIAL PRIMARY KEY,
user_id BIGINT,
activity_date DATE,
message_count INTEGER DEFAULT 0,
last_activity_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, activity_date)
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS central_bank (
id INTEGER PRIMARY KEY DEFAULT 1,
capital REAL DEFAULT 1000.0,
total_taxes_collected REAL DEFAULT 0.0,
total_inflation_adjustments REAL DEFAULT 0.0,
total_emissions REAL DEFAULT 0.0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (id)
)
''')
cur.execute('''
CREATE TABLE IF NOT EXISTS penis_stats (
user_id BIGINT PRIMARY KEY,
display_name TEXT NOT NULL DEFAULT '',
last_used_ts BIGINT
)
''')
default_settings = {
'central_bank_rate': '7.5',
'deposit_min_amount': '10.0',
'deposit_min_days': '7',
'loan_max_amount': '100.0',
'loan_interest_rate': '15.0',
'tax_rate': '13.0',
'transfer_limit_percent': '20.0',
'inflation_rate': '5.0',
'vanomasa_cooldown': '30',
'message_reward_rate': '0.01',
'last_activity_payout': '',
}
for key, value in default_settings.items():
cur.execute('''
INSERT INTO economy_settings (key, value) VALUES (%s, %s)
ON CONFLICT (key) DO NOTHING
''', (key, value))
cur.execute('''
INSERT INTO central_bank (id, capital) VALUES (1, 1000.0)
ON CONFLICT (id) DO NOTHING
''')
def get_user_balance(self, user_id: int) -> float:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,))
result = cur.fetchone()
return result['balance'] if result else 0.0
def update_balance(self, user_id: int, amount: float, transaction_type: str, description: str = ""):
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s
''', (user_id, amount, amount))
cur.execute('''
INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description)
VALUES (%s, %s, %s, %s, %s)
''', (user_id, user_id, amount, transaction_type, description))
logger.info(f"Balance updated for user {user_id}: +{amount} ({transaction_type})")
def transfer_money(self, from_user_id: int, to_user_id: int, amount: float, description: str = "") -> bool:
if amount <= 0:
return False
from_balance = self.get_user_balance(from_user_id)
if amount > from_balance * 0.2 or from_balance < amount:
return False
with get_conn() as conn:
cur = conn.cursor()
cur.execute(
'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s',
(amount, from_user_id),
)
cur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s
''', (to_user_id, amount, amount))
cur.execute('''
INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description)
VALUES (%s, %s, %s, 'transfer', %s)
''', (from_user_id, to_user_id, amount, description))
logger.info(f"Transfer: {amount} from user {from_user_id} to user {to_user_id}")
return True
def create_deposit(self, user_id: int, amount: float, days: int) -> bool:
if amount <= 0:
return False
balance = self.get_user_balance(user_id)
min_deposit = float(self.get_setting('deposit_min_amount'))
if amount < min_deposit or balance < amount:
return False
central_bank_rate = float(self.get_setting('central_bank_rate'))
matures_at = datetime.now() + timedelta(days=days)
with get_conn() as conn:
cur = conn.cursor()
cur.execute(
'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s',
(amount, user_id),
)
cur.execute('''
UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (amount,))
cur.execute('''
INSERT INTO deposits (user_id, amount, interest_rate, matures_at)
VALUES (%s, %s, %s, %s)
''', (user_id, amount, central_bank_rate, matures_at))
logger.info(f"Deposit created: user {user_id}, amount {amount}, rate {central_bank_rate}%")
return True
def create_loan(self, user_id: int, amount: float, days: int) -> bool:
if amount <= 0:
return False
max_loan = float(self.get_setting('loan_max_amount'))
loan_rate = float(self.get_setting('loan_interest_rate'))
if amount > max_loan:
return False
due_at = datetime.now() + timedelta(days=days)
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
UPDATE central_bank SET capital = capital - %s, last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (amount,))
cur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s
''', (user_id, amount, amount))
cur.execute('''
INSERT INTO loans (user_id, amount, interest_rate, due_at)
VALUES (%s, %s, %s, %s)
''', (user_id, amount, loan_rate, due_at))
logger.info(f"Loan created: user {user_id}, amount {amount}, rate {loan_rate}%")
return True
def repay_loan(self, user_id: int, loan_id: int) -> bool:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
'SELECT amount, interest_rate, is_repaid FROM loans WHERE id = %s AND user_id = %s',
(loan_id, user_id),
)
loan = cur.fetchone()
if not loan or loan['is_repaid']:
return False
total_repayment = loan['amount'] * (1 + loan['interest_rate'] / 100)
balance = self.get_user_balance(user_id)
if balance < total_repayment:
return False
wcur = conn.cursor()
wcur.execute(
'UPDATE user_balances SET balance = balance - %s WHERE user_id = %s',
(total_repayment, user_id),
)
wcur.execute('''
UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (total_repayment,))
wcur.execute('UPDATE loans SET is_repaid = TRUE WHERE id = %s', (loan_id,))
logger.info(f"Loan repaid: user {user_id}, loan {loan_id}, amount {total_repayment}")
return True
def collect_daily_taxes(self):
tax_rate = float(self.get_setting('tax_rate')) / 100
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('''
SELECT user_id, daily_income FROM user_balances
WHERE daily_income > 0 AND (last_daily_reset != CURRENT_DATE OR last_daily_reset IS NULL)
''')
users = cur.fetchall()
total_taxes_collected = 0.0
wcur = conn.cursor()
for row in users:
user_id = row['user_id']
daily_income = row['daily_income']
tax_amount = daily_income * tax_rate
if tax_amount <= 0:
continue
wcur.execute('''
UPDATE user_balances
SET balance = balance - %s, daily_income = 0, last_daily_reset = CURRENT_DATE
WHERE user_id = %s
''', (tax_amount, user_id))
wcur.execute(
'INSERT INTO taxes (user_id, amount, tax_date) VALUES (%s, %s, CURRENT_DATE)',
(user_id, tax_amount),
)
total_taxes_collected += tax_amount
if total_taxes_collected > 0:
wcur.execute('''
UPDATE central_bank
SET capital = capital + %s, total_taxes_collected = total_taxes_collected + %s
WHERE id = 1
''', (total_taxes_collected, total_taxes_collected))
logger.info(f"Daily taxes collected: {total_taxes_collected:.2f} cm from {len(users)} users")
def count_message(self, user_id: int):
today = datetime.now().strftime('%Y-%m-%d')
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
INSERT INTO daily_activity (user_id, activity_date, message_count, last_activity_time)
VALUES (%s, %s, 1, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, activity_date) DO UPDATE SET
message_count = daily_activity.message_count + 1,
last_activity_time = CURRENT_TIMESTAMP
''', (user_id, today))
def pay_daily_activity_rewards(self) -> int:
reward_rate = float(self.get_setting('message_reward_rate'))
last_payout = self.get_setting('last_activity_payout')
today = datetime.now().strftime('%Y-%m-%d')
if last_payout == today:
return 0
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('''
SELECT user_id, message_count FROM daily_activity
WHERE activity_date = %s AND message_count > 0
''', (yesterday,))
users = cur.fetchall()
total_paid = 0.0
wcur = conn.cursor()
for row in users:
user_id = row['user_id']
message_count = row['message_count']
reward = message_count * reward_rate
if reward <= 0:
continue
wcur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET
balance = user_balances.balance + %s,
daily_income = user_balances.daily_income + %s
''', (user_id, reward, reward, reward))
wcur.execute('''
INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description)
VALUES (%s, %s, %s, 'activity_reward', %s)
''', (user_id, user_id, reward, f'Вознаграждение за {message_count} сообщений'))
total_paid += reward
wcur.execute(
'UPDATE economy_settings SET value = %s WHERE key = %s',
(today, 'last_activity_payout'),
)
logger.info(f"Activity rewards paid: {total_paid:.2f} cm to {len(users)} users")
return len(users)
def get_central_bank_stats(self) -> Dict:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('''
SELECT capital, total_taxes_collected, total_inflation_adjustments, total_emissions
FROM central_bank WHERE id = 1
''')
result = cur.fetchone()
if result:
return {
'capital': result['capital'],
'total_taxes_collected': result['total_taxes_collected'],
'total_inflation_adjustments': result['total_inflation_adjustments'],
'total_emissions': result['total_emissions'],
}
return {
'capital': 1000.0,
'total_taxes_collected': 0.0,
'total_inflation_adjustments': 0.0,
'total_emissions': 0.0,
}
def update_central_bank(self, capital_change: float, transaction_type: str, description: str = ""):
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
UPDATE central_bank SET capital = capital + %s, last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (capital_change,))
if transaction_type == 'tax':
cur.execute('''
UPDATE central_bank
SET total_taxes_collected = total_taxes_collected + %s WHERE id = 1
''', (abs(capital_change),))
elif transaction_type == 'inflation':
cur.execute('''
UPDATE central_bank
SET total_inflation_adjustments = total_inflation_adjustments + %s WHERE id = 1
''', (abs(capital_change),))
elif transaction_type == 'emission':
cur.execute('''
UPDATE central_bank
SET total_emissions = total_emissions + %s WHERE id = 1
''', (abs(capital_change),))
logger.info(f"Central bank updated: {capital_change} ({transaction_type})")
def emit_money(self, amount: float, reason: str = "") -> bool:
if self.get_central_bank_stats()['capital'] < amount:
return False
self.update_central_bank(-amount, 'emission', reason)
logger.info(f"Money emitted: {amount} cm ({reason})")
return True
def vanomasa(self) -> bool:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT value FROM economy_settings WHERE key = %s", ('last_vanomasa',))
last_vanomasa = cur.fetchone()
if last_vanomasa and last_vanomasa['value']:
try:
last_date = datetime.strptime(last_vanomasa['value'], '%Y-%m-%d')
cooldown_days = int(self.get_setting('vanomasa_cooldown'))
if datetime.now() - last_date < timedelta(days=cooldown_days):
return False
except ValueError:
pass
today = datetime.now().strftime('%Y-%m-%d')
wcur = conn.cursor()
wcur.execute('UPDATE user_balances SET balance = 0, daily_income = 0')
wcur.execute('''
INSERT INTO economy_settings (key, value, updated_at) VALUES (%s, %s, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP
''', ('last_vanomasa', today))
logger.warning("VANOMASA: All balances reset to zero!")
return True
def get_setting(self, key: str) -> str:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('SELECT value FROM economy_settings WHERE key = %s', (key,))
result = cur.fetchone()
return result['value'] if result else '0'
def update_setting(self, key: str, value: str):
with get_conn() as conn:
cur = conn.cursor()
cur.execute('''
INSERT INTO economy_settings (key, value, updated_at) VALUES (%s, %s, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP
''', (key, value))
def get_user_stats(self, user_id: int) -> Dict:
today = datetime.now().strftime('%Y-%m-%d')
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,))
row = cur.fetchone()
balance = row['balance'] if row else 0.0
cur.execute('''
SELECT COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total
FROM deposits WHERE user_id = %s AND is_active = TRUE
''', (user_id,))
deposits = cur.fetchone()
cur.execute('''
SELECT COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total
FROM loans WHERE user_id = %s AND is_repaid = FALSE
''', (user_id,))
loans = cur.fetchone()
cur.execute(
'SELECT message_count FROM daily_activity WHERE user_id = %s AND activity_date = %s',
(user_id, today),
)
today_row = cur.fetchone()
cur.execute(
'SELECT message_count FROM daily_activity WHERE user_id = %s AND activity_date = %s',
(user_id, yesterday),
)
yesterday_row = cur.fetchone()
return {
'balance': balance,
'active_deposits_count': deposits['cnt'] or 0,
'active_deposits_sum': deposits['total'] or 0.0,
'active_loans_count': loans['cnt'] or 0,
'active_loans_sum': loans['total'] or 0.0,
'today_messages': today_row['message_count'] if today_row else 0,
'yesterday_messages': yesterday_row['message_count'] if yesterday_row else 0,
}
def regulate_inflation(self):
inflation_rate = float(self.get_setting('inflation_rate'))
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('SELECT SUM(balance) AS total_balance, COUNT(*) AS user_count FROM user_balances')
result = cur.fetchone()
total_supply = result['total_balance'] or 0.0
cur.execute('SELECT capital FROM central_bank WHERE id = 1')
cb_result = cur.fetchone()
cb_capital = cb_result['capital'] if cb_result else 1000.0
if inflation_rate > 10.0 and total_supply > 1000 and cb_capital > 100:
buyback_amount = min(total_supply * 0.05, cb_capital * 0.1)
self.update_central_bank(-buyback_amount, 'inflation', f'Anti-inflation: {buyback_amount:.2f} cm')
new_tax_rate = min(20.0, 13.0 + (inflation_rate - 10.0) * 0.5)
self.update_setting('tax_rate', str(new_tax_rate))
logger.info(f"Inflation regulation: bought back {buyback_amount:.2f} cm, tax={new_tax_rate}%")
elif inflation_rate < 2.0 and total_supply < 500 and cb_capital > 200:
emission_amount = cb_capital * 0.05
self.update_central_bank(-emission_amount, 'emission', f'Stimulus: {emission_amount:.2f} cm')
new_tax_rate = max(10.0, 13.0 - (2.0 - inflation_rate) * 1.5)
self.update_setting('tax_rate', str(new_tax_rate))
logger.info(f"Economic stimulus: emitted {emission_amount:.2f} cm, tax={new_tax_rate}%")
def play_penis(self, user_id: int, display_name: str, now_ts: int,
start_length: float, cooldown_seconds: int,
min_delta: float, max_delta: float) -> dict:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('SELECT balance FROM user_balances WHERE user_id = %s', (user_id,))
row = cur.fetchone()
current_balance = row['balance'] if row else start_length
cur.execute(
'SELECT last_used_ts, display_name FROM penis_stats WHERE user_id = %s',
(user_id,),
)
ps_row = cur.fetchone()
last_used_ts = ps_row['last_used_ts'] if ps_row else None
if last_used_ts is not None:
next_ts = int(last_used_ts) + cooldown_seconds
if now_ts < next_ts:
if ps_row and ps_row['display_name'] != display_name:
wcur = conn.cursor()
wcur.execute(
'UPDATE penis_stats SET display_name = %s WHERE user_id = %s',
(display_name, user_id),
)
return {'allowed': False, 'left_seconds': next_ts - now_ts}
delta = round(random.uniform(min_delta, max_delta), 1)
sign = random.choice([-1, 1])
new_balance = round(max(0.1, current_balance + sign * delta), 1)
wcur = conn.cursor()
wcur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET balance = %s
''', (user_id, new_balance, new_balance))
sign_label = f'+{delta:.1f}' if sign > 0 else f'-{delta:.1f}'
wcur.execute('''
INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description)
VALUES (%s, %s, %s, 'penis_game', %s)
''', (user_id, user_id, sign * delta, f'Игра /penis: {sign_label} см'))
if ps_row is None:
wcur.execute(
'INSERT INTO penis_stats (user_id, display_name, last_used_ts) VALUES (%s, %s, %s)',
(user_id, display_name, now_ts),
)
else:
wcur.execute(
'UPDATE penis_stats SET display_name = %s, last_used_ts = %s WHERE user_id = %s',
(display_name, now_ts, user_id),
)
return {'allowed': True, 'new_balance': new_balance, 'delta': delta, 'sign': sign}
def get_penis_top(self, limit: int) -> list:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('''
SELECT ub.user_id, ps.display_name, ub.balance
FROM user_balances ub
INNER JOIN penis_stats ps ON ub.user_id = ps.user_id
ORDER BY ub.balance DESC, ub.user_id ASC
LIMIT %s
''', (limit,))
rows = cur.fetchall()
return [{'user_id': r['user_id'], 'display_name': r['display_name'], 'balance': r['balance']}
for r in rows]
def process_matured_deposits(self) -> int:
with get_conn() as conn:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute('''
SELECT id, user_id, amount, interest_rate
FROM deposits
WHERE is_active = TRUE AND matures_at <= NOW()
''')
matured = cur.fetchall()
paid = 0
wcur = conn.cursor()
for row in matured:
dep_id = row['id']
user_id = row['user_id']
amount = row['amount']
rate = row['interest_rate']
payout = round(amount * (1 + rate / 100 * 1 / 365 * 30), 2)
wcur.execute('''
INSERT INTO user_balances (user_id, balance, daily_income, last_daily_reset)
VALUES (%s, %s, 0.0, CURRENT_DATE)
ON CONFLICT (user_id) DO UPDATE SET balance = user_balances.balance + %s
''', (user_id, payout, payout))
wcur.execute('''
UPDATE central_bank SET capital = capital - %s, last_updated = CURRENT_TIMESTAMP
WHERE id = 1
''', (payout,))
wcur.execute('UPDATE deposits SET is_active = FALSE WHERE id = %s', (dep_id,))
wcur.execute('''
INSERT INTO transactions (from_user_id, to_user_id, amount, transaction_type, description)
VALUES (0, %s, %s, 'deposit_payout', %s)
''', (user_id, payout, f'Выплата вклада #{dep_id}: {amount:.1f} + проценты = {payout:.2f} см'))
paid += 1
if paid:
logger.info(f"Matured deposits paid out: {paid}")
return paid
economy = EconomyManager()