Загрузить файлы в «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
Binary file not shown.
+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()
+213
View File
@@ -0,0 +1,213 @@
-- Создаем таблицу для отслеживания отправленных уведомлений об окончании подписки
CREATE TABLE IF NOT EXISTS subscription_notifications (
id SERIAL PRIMARY KEY,
parent_id BIGINT NOT NULL REFERENCES parents(user_id) ON DELETE CASCADE,
notified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
days_left INTEGER NOT NULL,
UNIQUE(parent_id, days_left) -- Чтобы не дублировать уведомления для одного количества дней
);
-- Создаем таблицу справочника школ
CREATE TABLE IF NOT EXISTS schools (
id SERIAL PRIMARY KEY,
school_number VARCHAR(50) NOT NULL UNIQUE,
school_name VARCHAR(255) NOT NULL,
address TEXT,
city VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Создаем таблицу родителей (пользователей)
CREATE TABLE IF NOT EXISTS parents (
user_id BIGINT PRIMARY KEY,
username VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Создаем таблицу детей
CREATE TABLE IF NOT EXISTS children (
id SERIAL PRIMARY KEY,
parent_id BIGINT NOT NULL REFERENCES parents(user_id) ON DELETE CASCADE,
card_number VARCHAR(16) NOT NULL,
student_name VARCHAR(255) NOT NULL,
school_id INTEGER NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(parent_id, card_number)
);
-- Индексы для детей
CREATE INDEX IF NOT EXISTS idx_children_parent_id ON children(parent_id);
CREATE INDEX IF NOT EXISTS idx_children_card_number ON children(card_number);
CREATE INDEX IF NOT EXISTS idx_children_school_id ON children(school_id);
CREATE INDEX IF NOT EXISTS idx_subscription_notifications_parent_id ON subscription_notifications(parent_id);
-- Создаем таблицу для истории проходов через турникет
CREATE TABLE IF NOT EXISTS access_events (
id SERIAL PRIMARY KEY,
event_id VARCHAR(100) UNIQUE,
user_id VARCHAR(100) NOT NULL,
device_id VARCHAR(100),
resource_number INTEGER NOT NULL,
access_zone_id1 VARCHAR(100),
access_zone_id2 VARCHAR(100),
school_id INTEGER REFERENCES schools(id) ON DELETE CASCADE,
event_time TIMESTAMP NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для access_events
CREATE INDEX IF NOT EXISTS idx_access_events_user_id ON access_events(user_id);
CREATE INDEX IF NOT EXISTS idx_access_events_time ON access_events(event_time);
CREATE INDEX IF NOT EXISTS idx_access_events_processed ON access_events(processed);
-- Создаем таблицу уведомлений
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
parent_id BIGINT NOT NULL REFERENCES parents(user_id) ON DELETE CASCADE,
child_id INTEGER REFERENCES children(id) ON DELETE CASCADE,
message TEXT NOT NULL,
event_type VARCHAR(50),
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для уведомлений
CREATE INDEX IF NOT EXISTS idx_notifications_parent_id ON notifications(parent_id);
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read);
-- ============================================
-- ПОДПИСОЧНАЯ СИСТЕМА
-- ============================================
-- Создаем таблицу подписок
CREATE TABLE IF NOT EXISTS subscriptions (
id SERIAL PRIMARY KEY,
parent_id BIGINT NOT NULL UNIQUE REFERENCES parents(user_id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'trial', -- 'trial', 'active', 'expired', 'cancelled'
trial_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
trial_end TIMESTAMP,
paid_until TIMESTAMP,
last_payment_date TIMESTAMP,
payment_amount DECIMAL(10, 2) DEFAULT 50.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для подписок
CREATE INDEX IF NOT EXISTS idx_subscriptions_parent_id ON subscriptions(parent_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
-- Создаем таблицу истории платежей
CREATE TABLE IF NOT EXISTS payments (
id SERIAL PRIMARY KEY,
parent_id BIGINT NOT NULL REFERENCES parents(user_id) ON DELETE CASCADE,
amount DECIMAL(10, 2) NOT NULL,
payment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
payment_method VARCHAR(50),
transaction_id VARCHAR(255) UNIQUE,
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'success', 'failed'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для платежей
CREATE INDEX IF NOT EXISTS idx_payments_parent_id ON payments(parent_id);
CREATE INDEX IF NOT EXISTS idx_payments_transaction_id ON payments(transaction_id);
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
-- ============================================
-- ФУНКЦИИ И ТРИГГЕРЫ
-- ============================================
-- Функция для автоматического обновления updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Триггер для обновления updated_at в parents
DROP TRIGGER IF EXISTS update_parents_updated_at ON parents;
CREATE TRIGGER update_parents_updated_at
BEFORE UPDATE ON parents
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Триггер для обновления updated_at в children
DROP TRIGGER IF EXISTS update_children_updated_at ON children;
CREATE TRIGGER update_children_updated_at
BEFORE UPDATE ON children
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Триггер для обновления updated_at в subscriptions
DROP TRIGGER IF EXISTS update_subscriptions_updated_at ON subscriptions;
CREATE TRIGGER update_subscriptions_updated_at
BEFORE UPDATE ON subscriptions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- НАЧАЛЬНЫЕ ДАННЫЕ
-- ============================================
-- Добавляем школы
INSERT INTO schools (school_number, school_name, address, city) VALUES
('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', 'Ростов-на-Дону')
ON CONFLICT (school_number) DO NOTHING;
-- Добавляем тестового пользователя (для разработки)
-- INSERT INTO parents (user_id, username) VALUES (15649081, 'Алексей Халецкий')
-- ON CONFLICT (user_id) DO NOTHING;
-- Добавляем тестовую подписку для разработчика (активная на год)
-- INSERT INTO subscriptions (parent_id, status, paid_until, payment_amount)
-- VALUES (15649081, 'active', CURRENT_TIMESTAMP + INTERVAL '365 days', 50.00)
-- ON CONFLICT (parent_id) DO NOTHING;
-- ============================================
-- ПРОВЕРОЧНЫЕ ЗАПРОСЫ
-- ============================================
-- Проверка количества школ
-- SELECT COUNT(*) as total_schools FROM schools;
-- Проверка подписок
-- SELECT
-- p.username,
-- s.status,
-- s.trial_end,
-- s.paid_until,
-- s.payment_amount
-- FROM subscriptions s
-- JOIN parents p ON s.parent_id = p.user_id;
-- Проверка детей
-- SELECT
-- p.username,
-- c.student_name,
-- c.card_number,
-- sc.school_name
-- FROM children c
-- JOIN parents p ON c.parent_id = p.user_id
-- JOIN schools sc ON c.school_id = sc.id;