Загрузить файлы в «database»

This commit is contained in:
2026-07-09 15:39:32 +00:00
parent 2904226cc4
commit aa126be6c0
3 changed files with 740 additions and 0 deletions
+527
View File
@@ -0,0 +1,527 @@
import pg8000
import os
from typing import Optional, Dict, List, Any
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
class Database:
def __init__(self):
self.conn = None
self.cursor = None
async def connect(self):
try:
self.conn = pg8000.connect(
host=os.getenv('DB_HOST', 'localhost'),
port=int(os.getenv('DB_PORT', '5432')),
database=os.getenv('DB_NAME', 'max_bot'),
user=os.getenv('DB_USER', 'max_user'),
password=os.getenv('DB_PASSWORD', 'max_password')
)
self.cursor = self.conn.cursor()
logger.info("✅ Подключение к PostgreSQL установлено (pg8000)")
await self.init_tables()
await self.init_schools()
except Exception as e:
logger.error(f"❌ Ошибка подключения к БД: {e}")
raise
async def disconnect(self):
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
logger.info("Соединение с PostgreSQL закрыто")
async def init_tables(self):
try:
sql_path = os.path.join(os.path.dirname(__file__), 'init.sql')
with open(sql_path, 'r') as f:
sql = f.read()
self.cursor.execute(sql)
self.conn.commit()
logger.info("✅ Таблицы созданы/проверены")
except Exception as e:
logger.error(f"❌ Ошибка инициализации таблиц: {e}")
raise
async def init_schools(self):
try:
self.cursor.execute("SELECT COUNT(*) FROM schools")
count = self.cursor.fetchone()[0]
if count == 0:
schools = [
('101', 'Средняя общеобразовательная школа №101', 'ул. Ленина, 15', 'Москва'),
('102', 'Средняя общеобразовательная школа №102', 'ул. Гагарина, 10', 'Москва'),
('103', 'Средняя общеобразовательная школа №103', 'ул. Мира, 25', 'Москва'),
('104', 'Гимназия №104', 'ул. Садовая, 8', 'Москва'),
('105', 'Лицей №105', 'ул. Тверская, 12', 'Москва'),
('201', 'Гимназия №201', 'ул. Пушкина, 5', 'Санкт-Петербург'),
('202', 'Средняя школа №202', 'ул. Невский проспект, 20', 'Санкт-Петербург'),
('203', 'Средняя школа №203', 'ул. Московская, 15', 'Санкт-Петербург'),
('301', 'Лицей №301', 'ул. Советская, 8', 'Казань'),
('302', 'Средняя школа №302', 'ул. Ленина, 42', 'Казань'),
('401', 'Средняя школа №401', 'ул. Мира, 12', 'Новосибирск'),
('402', 'Гимназия №402', 'ул. Кирова, 7', 'Новосибирск'),
('501', 'Школа №501 с углубленным изучением математики', 'ул. Лермонтова, 3', 'Екатеринбург'),
('502', 'Гимназия №502', 'ул. Свердлова, 10', 'Екатеринбург'),
('601', 'Средняя общеобразовательная школа №601', 'ул. Садовая, 25', 'Нижний Новгород'),
('602', 'Лицей №602', 'ул. Горького, 8', 'Нижний Новгород'),
('701', 'Гимназия №701', 'ул. Чехова, 10', 'Ростов-на-Дону'),
('702', 'Средняя школа №702', 'ул. Пушкинская, 15', 'Ростов-на-Дону'),
]
for school in schools:
self.cursor.execute("""
INSERT INTO schools (school_number, school_name, address, city)
VALUES (%s, %s, %s, %s)
""", school)
self.conn.commit()
logger.info(f"✅ Добавлено {len(schools)} школ в справочник")
except Exception as e:
logger.error(f"❌ Ошибка инициализации школ: {e}")
async def get_parent(self, user_id: int) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM parents WHERE user_id = %s", (user_id,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def create_parent(self, user_id: int, username: str) -> Dict:
self.cursor.execute("""
INSERT INTO parents (user_id, username)
VALUES (%s, %s)
ON CONFLICT (user_id) DO UPDATE
SET username = %s, updated_at = CURRENT_TIMESTAMP
RETURNING *
""", (user_id, username, username))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
async def get_all_schools(self) -> List[Dict]:
self.cursor.execute("SELECT * FROM schools ORDER BY school_number")
rows = self.cursor.fetchall()
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
async def get_school_by_number(self, school_number: str) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM schools WHERE school_number = %s", (school_number,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def get_school_by_id(self, school_id: int) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM schools WHERE id = %s", (school_id,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def search_schools(self, query: str) -> List[Dict]:
self.cursor.execute("""
SELECT * FROM schools
WHERE school_number ILIKE %s OR school_name ILIKE %s
ORDER BY school_number
""", (f'%{query}%', f'%{query}%'))
rows = self.cursor.fetchall()
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
async def get_children(self, parent_id: int) -> List[Dict]:
self.cursor.execute("""
SELECT
c.id, c.parent_id, c.card_number, c.student_name, c.school_id,
s.school_number, s.school_name, s.address, s.city,
c.created_at, c.updated_at
FROM children c
JOIN schools s ON c.school_id = s.id
WHERE c.parent_id = %s
ORDER BY c.id
""", (parent_id,))
rows = self.cursor.fetchall()
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
async def get_child_by_card_number(self, card_number: str) -> Optional[Dict]:
self.cursor.execute("""
SELECT
c.id, c.parent_id, c.card_number, c.student_name, c.school_id,
s.school_number, s.school_name
FROM children c
JOIN schools s ON c.school_id = s.id
WHERE c.card_number = %s
""", (card_number,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def add_child(self, parent_id: int, card_number: str, student_name: str, school_number: str) -> Dict:
parent = await self.get_parent(parent_id)
if not parent:
raise ValueError("Родитель не найден. Сначала отправьте /start")
school = await self.get_school_by_number(school_number)
if not school:
raise ValueError(f"Школа с номером '{school_number}' не найдена в справочнике")
self.cursor.execute("""
INSERT INTO children (parent_id, card_number, student_name, school_id)
VALUES (%s, %s, %s, %s)
ON CONFLICT (parent_id, card_number) DO UPDATE
SET student_name = %s, school_id = %s, updated_at = CURRENT_TIMESTAMP
RETURNING *
""", (parent_id, card_number, student_name, school['id'], student_name, school['id']))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
child = dict(zip(columns, row))
child['school_number'] = school['school_number']
child['school_name'] = school['school_name']
return child
async def delete_child(self, parent_id: int, child_id: int) -> bool:
self.cursor.execute("DELETE FROM children WHERE parent_id = %s AND id = %s", (parent_id, child_id))
self.conn.commit()
return self.cursor.rowcount > 0
async def get_access_event_by_id(self, event_id: str) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM access_events WHERE event_id = %s", (event_id,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def save_access_event(self, event_id: str, user_id: str, resource_number: int,
event_time: datetime, device_id: str = None,
access_zone_id1: str = None, access_zone_id2: str = None) -> Dict:
self.cursor.execute("""
INSERT INTO access_events
(event_id, user_id, device_id, resource_number, access_zone_id1, access_zone_id2, event_time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (event_id) DO NOTHING
RETURNING *
""", (event_id, user_id, device_id, resource_number, access_zone_id1, access_zone_id2, event_time))
self.conn.commit()
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
existing = await self.get_access_event_by_id(event_id)
return existing if existing else {}
async def mark_event_processed(self, event_id: int) -> bool:
self.cursor.execute("UPDATE access_events SET processed = TRUE WHERE id = %s", (event_id,))
self.conn.commit()
return self.cursor.rowcount > 0
async def save_notification(self, parent_id: int, child_id: int, message: str, event_type: str) -> Dict:
self.cursor.execute("""
INSERT INTO notifications (parent_id, child_id, message, event_type)
VALUES (%s, %s, %s, %s)
RETURNING *
""", (parent_id, child_id, message, event_type))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
async def get_unread_notifications(self, parent_id: int) -> List[Dict]:
self.cursor.execute("""
SELECT * FROM notifications
WHERE parent_id = %s AND is_read = FALSE
ORDER BY created_at DESC
""", (parent_id,))
rows = self.cursor.fetchall()
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
async def mark_notification_as_read(self, notification_id: int) -> bool:
self.cursor.execute("UPDATE notifications SET is_read = TRUE WHERE id = %s", (notification_id,))
self.conn.commit()
return self.cursor.rowcount > 0
# --- ПОДПИСОЧНАЯ СИСТЕМА ---
async def get_subscription(self, parent_id: int) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM subscriptions WHERE parent_id = %s", (parent_id,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def create_subscription(self, parent_id: int) -> Dict:
trial_end = datetime.now() + timedelta(days=7)
self.cursor.execute("""
INSERT INTO subscriptions (parent_id, status, trial_start, trial_end)
VALUES (%s, %s, %s, %s)
ON CONFLICT (parent_id) DO UPDATE
SET status = 'trial', trial_start = CURRENT_TIMESTAMP, trial_end = %s, updated_at = CURRENT_TIMESTAMP
RETURNING *
""", (parent_id, 'trial', datetime.now(), trial_end, trial_end))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
async def check_subscription_status(self, parent_id: int) -> Dict:
subscription = await self.get_subscription(parent_id)
if not subscription:
subscription = await self.create_subscription(parent_id)
now = datetime.now()
status = subscription.get('status')
if status == 'trial':
trial_end = subscription.get('trial_end')
if trial_end and trial_end < now:
await self.update_subscription_status(parent_id, 'expired')
subscription['status'] = 'expired'
elif status == 'active':
paid_until = subscription.get('paid_until')
if paid_until and paid_until < now:
await self.update_subscription_status(parent_id, 'expired')
subscription['status'] = 'expired'
return subscription
async def update_subscription_status(self, parent_id: int, status: str) -> bool:
self.cursor.execute("""
UPDATE subscriptions
SET status = %s, updated_at = CURRENT_TIMESTAMP
WHERE parent_id = %s
""", (status, parent_id))
self.conn.commit()
return self.cursor.rowcount > 0
async def extend_subscription(self, parent_id: int, months: int = 1) -> Dict:
try:
subscription = await self.get_subscription(parent_id)
if not subscription:
subscription = await self.create_subscription(parent_id)
current_end = subscription.get('paid_until')
if current_end and current_end > datetime.now():
paid_until = current_end + timedelta(days=30 * months)
else:
paid_until = datetime.now() + timedelta(days=30 * months)
self.cursor.execute("""
UPDATE subscriptions
SET status = 'active', paid_until = %s, last_payment_date = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE parent_id = %s
RETURNING *
""", (paid_until, parent_id))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
except Exception as e:
self.conn.rollback()
logger.error(f"❌ Ошибка продления подписки: {e}")
raise
# --- ПЛАТЕЖИ ---
async def record_payment(self, parent_id: int, amount: float, transaction_id: str, status: str = 'pending') -> Dict:
"""Запись платежа с проверкой на дубликат"""
try:
# Сначала проверяем, существует ли платеж
existing = await self.get_payment_by_transaction_id(transaction_id)
if existing:
if existing.get('status') != status:
await self.update_payment_status(transaction_id, status)
return await self.get_payment_by_transaction_id(transaction_id)
return existing
self.cursor.execute("""
INSERT INTO payments (parent_id, amount, transaction_id, status)
VALUES (%s, %s, %s, %s)
RETURNING *
""", (parent_id, amount, transaction_id, status))
self.conn.commit()
row = self.cursor.fetchone()
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
except Exception as e:
# Если ошибка уникальности, пытаемся получить существующий платеж
if 'duplicate key' in str(e).lower():
self.conn.rollback()
existing = await self.get_payment_by_transaction_id(transaction_id)
if existing:
return existing
self.conn.rollback()
logger.error(f"❌ Ошибка записи платежа: {e}")
raise
async def get_payment_by_transaction_id(self, transaction_id: str) -> Optional[Dict]:
self.cursor.execute("SELECT * FROM payments WHERE transaction_id = %s", (transaction_id,))
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return None
async def update_payment_status(self, transaction_id: str, status: str) -> bool:
try:
self.cursor.execute("""
UPDATE payments
SET status = %s
WHERE transaction_id = %s
""", (status, transaction_id))
self.conn.commit()
return self.cursor.rowcount > 0
except Exception as e:
self.conn.rollback()
logger.error(f"❌ Ошибка обновления статуса платежа: {e}")
return False
async def get_expiring_subscriptions(self, days_threshold: int = 3) -> List[Dict]:
"""
Получение подписок, у которых до окончания осталось days_threshold дней или меньше.
"""
now = datetime.now()
threshold_date = now + timedelta(days=days_threshold)
self.cursor.execute("""
SELECT
s.*,
p.username,
p.user_id
FROM subscriptions s
JOIN parents p ON s.parent_id = p.user_id
WHERE s.status IN ('trial', 'active')
AND s.trial_end <= %s
AND s.trial_end > %s
UNION
SELECT
s.*,
p.username,
p.user_id
FROM subscriptions s
JOIN parents p ON s.parent_id = p.user_id
WHERE s.status = 'active'
AND s.paid_until <= %s
AND s.paid_until > %s
""", (threshold_date, now, threshold_date, now))
rows = self.cursor.fetchall()
if rows:
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
return []
async def get_subscription_notification_sent(self, parent_id: int, days_left: int) -> bool:
"""Проверка, отправлялось ли уже уведомление об окончании подписки для данного количества дней"""
self.cursor.execute("""
SELECT 1 FROM subscription_notifications
WHERE parent_id = %s AND days_left = %s
""", (parent_id, days_left))
return self.cursor.fetchone() is not None
async def get_expiring_subscriptions(self, days_threshold: int = 3) -> List[Dict]:
"""
Получение подписок, у которых до окончания осталось days_threshold дней или меньше.
"""
now = datetime.now()
threshold_date = now + timedelta(days=days_threshold)
self.cursor.execute("""
SELECT
s.*,
p.username,
p.user_id
FROM subscriptions s
JOIN parents p ON s.parent_id = p.user_id
WHERE s.status IN ('trial', 'active')
AND s.trial_end <= %s
AND s.trial_end > %s
UNION
SELECT
s.*,
p.username,
p.user_id
FROM subscriptions s
JOIN parents p ON s.parent_id = p.user_id
WHERE s.status = 'active'
AND s.paid_until <= %s
AND s.paid_until > %s
""", (threshold_date, now, threshold_date, now))
rows = self.cursor.fetchall()
if rows:
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
return []
async def get_subscription_notification_sent(self, parent_id: int, days_left: int) -> bool:
"""Проверка, отправлялось ли уже уведомление об окончании подписки для данного количества дней"""
self.cursor.execute("""
SELECT 1 FROM subscription_notifications
WHERE parent_id = %s AND days_left = %s
""", (parent_id, days_left))
return self.cursor.fetchone() is not None
async def mark_subscription_notification_sent(self, parent_id: int, days_left: int) -> Dict:
"""Отметить, что уведомление об окончании подписки отправлено"""
self.cursor.execute("""
INSERT INTO subscription_notifications (parent_id, days_left)
VALUES (%s, %s)
ON CONFLICT (parent_id, days_left) DO NOTHING
RETURNING *
""", (parent_id, days_left))
self.conn.commit()
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return {}
async def clean_old_notifications(self, days: int = 30):
"""Очистка старых записей уведомлений (старше days дней)"""
self.cursor.execute("""
DELETE FROM subscription_notifications
WHERE notified_at < CURRENT_DATE - INTERVAL '%s days'
""", (days,))
self.conn.commit()
return self.cursor.rowcount
async def mark_subscription_notification_sent(self, parent_id: int, days_left: int) -> Dict:
"""Отметить, что уведомление об окончании подписки отправлено"""
self.cursor.execute("""
INSERT INTO subscription_notifications (parent_id, days_left)
VALUES (%s, %s)
ON CONFLICT (parent_id, days_left) DO NOTHING
RETURNING *
""", (parent_id, days_left))
self.conn.commit()
row = self.cursor.fetchone()
if row:
columns = [desc[0] for desc in self.cursor.description]
return dict(zip(columns, row))
return {}
async def clean_old_notifications(self, days: int = 30):
"""Очистка старых записей уведомлений (старше days дней)"""
self.cursor.execute("""
DELETE FROM subscription_notifications
WHERE notified_at < CURRENT_DATE - INTERVAL '%s days'
""", (days,))
self.conn.commit()
return self.cursor.rowcount
async def get_payment_history(self, parent_id: int, limit: int = 10) -> List[Dict]:
self.cursor.execute("""
SELECT * FROM payments
WHERE parent_id = %s
ORDER BY payment_date DESC
LIMIT %s
""", (parent_id, limit))
rows = self.cursor.fetchall()
columns = [desc[0] for desc in self.cursor.description]
return [dict(zip(columns, row)) for row in rows]
db = Database()