Добавлены функции для сохранения и обновления данных пользователей в классе OracleDatabase и обновлены обработчики в main.py:

- Реализован метод save_user для сохранения или обновления информации о пользователе.
- Добавлены вызовы save_user в обработчики команд и событий для отслеживания взаимодействий пользователей.
- Добавлен обработчик admin_stats для получения статистики пользователей по запросу администратора.
This commit is contained in:
Vlad 2025-05-25 11:03:37 +03:00
parent b1ba974b44
commit 3e175ff913
4 changed files with 531 additions and 10 deletions

200
README_USER_TRACKING.md Normal file
View File

@ -0,0 +1,200 @@
# 📊 Система учета пользователей SalvageDB Bot
## 🎯 Обзор
Данная система реализует полный учет пользователей Telegram бота в соответствии с требованиями KYC (Know Your Customer) для сервисов, принимающих платежи.
## 📝 Что отслеживается
### Данные пользователя Telegram:
- **ID пользователя** (уникальный 64-bit идентификатор)
- **Имя и фамилия** пользователя
- **Username** (если есть)
- **Код языка** (IETF language tag)
- **Статус Premium** в Telegram
- **Флаг бота** (для различения ботов и пользователей)
### Данные взаимодействий:
- **Дата первого взаимодействия**
- **Дата последнего взаимодействия**
- **Количество взаимодействий**
- **Источник регистрации** (какая команда/кнопка)
### Финансовые данные:
- **Общая сумма платежей** в Telegram Stars
- **Количество успешных платежей**
- **История транзакций**
## 🗄️ Структура базы данных
### Таблица `bot_users`
```sql
CREATE TABLE bot_users (
id NUMBER(19) PRIMARY KEY, -- Telegram user ID
first_name VARCHAR2(256) NOT NULL, -- Имя пользователя
last_name VARCHAR2(256), -- Фамилия (опционально)
username VARCHAR2(128), -- Username (опционально)
language_code VARCHAR2(10), -- Код языка
is_bot NUMBER(1) DEFAULT 0 NOT NULL, -- Флаг бота
is_premium NUMBER(1) DEFAULT 0, -- Telegram Premium
added_to_attachment_menu NUMBER(1) DEFAULT 0, -- Добавлен в меню вложений
first_interaction_date DATE DEFAULT SYSDATE, -- Первое взаимодействие
last_interaction_date DATE DEFAULT SYSDATE, -- Последнее взаимодействие
interaction_count NUMBER(10) DEFAULT 1, -- Количество взаимодействий
is_active NUMBER(1) DEFAULT 1, -- Активен ли пользователь
is_blocked NUMBER(1) DEFAULT 0, -- Заблокирован ли
registration_source VARCHAR2(50) DEFAULT 'bot', -- Источник регистрации
total_payments NUMBER(10,2) DEFAULT 0, -- Сумма платежей
successful_payments_count NUMBER(8) DEFAULT 0, -- Количество платежей
created_at DATE DEFAULT SYSDATE, -- Дата создания записи
updated_at DATE DEFAULT SYSDATE -- Дата обновления
);
```
## 🚀 Развертывание
### 1. Создание таблицы в Oracle
```bash
# Выполните SQL скрипт для создания таблицы
sqlplus username/password@database @create_users_table.sql
```
### 2. Настройка переменных окружения
Добавьте в ваш `.env` файл:
```env
# Настройки базы данных (уже должны быть)
db_user=your_oracle_user
db_password=your_oracle_password
db_dsn=your_oracle_dsn
# ID администратора для команды /admin_stats
ADMIN_USER_ID=123456789
```
### 3. Обновление кода
Код уже интегрирован в основные файлы:
- `main.py` - обработчики с сохранением данных пользователей
- `db.py` - методы для работы с пользователями
- `create_users_table.sql` - SQL скрипт для создания таблицы
## 📈 Использование
### Автоматическое отслеживание
Система автоматически сохраняет данные пользователя при:
- Использовании команды `/start`
- Нажатии любых кнопок
- Обработке VIN
- Совершении платежей
### Команды администратора
```
/admin_stats - Просмотр статистики пользователей
```
Пример вывода:
```
📊 Bot Users Statistics
👥 Users Overview:
• Total users: 1,234
• Premium users: 156
💰 Revenue:
• Total revenue: 567 ⭐️
• Total transactions: 445
📈 Activity:
• Active last 24h: 89
• Active last week: 234
```
## 🔧 API методы
### Сохранение пользователя
```python
await db.save_user(user: User, interaction_source: str = "bot") -> bool
```
### Обновление платежных данных
```python
await db.update_user_payment(user_id: int, payment_amount: float) -> bool
```
### Получение статистики пользователя
```python
await db.get_user_stats(user_id: int) -> Optional[dict]
```
### Общая статистика
```python
await db.get_users_summary() -> dict
```
## 🛡️ Безопасность и соответствие
### GDPR/Персональные данные:
- Сохраняются только данные, предоставляемые Telegram API
- Данные используются только для предоставления сервиса
- Пользователи могут быть помечены как неактивные
### Финансовая отчетность:
- Все платежи логируются с timestamp
- Связь платежей с пользователями для аудита
- Возможность генерации отчетов
### Мониторинг:
- Отслеживание активности пользователей
- Выявление подозрительной активности
- Статистика для бизнес-аналитики
## 📊 Полезные запросы
### Топ пользователей по платежам:
```sql
SELECT first_name, last_name, username, total_payments, successful_payments_count
FROM bot_users
WHERE total_payments > 0
ORDER BY total_payments DESC
FETCH FIRST 10 ROWS ONLY;
```
### Активные пользователи за месяц:
```sql
SELECT COUNT(*) as active_users
FROM bot_users
WHERE last_interaction_date >= SYSDATE - 30
AND is_active = 1;
```
### Конверсия в платящих пользователей:
```sql
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) as paying_users,
ROUND(COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) * 100.0 / COUNT(*), 2) as conversion_rate
FROM bot_users
WHERE is_active = 1;
```
## 🔍 Мониторинг и алерты
Рекомендуется настроить мониторинг для:
- Необычно высокой активности от одного пользователя
- Большого количества возвратов платежей
- Резкого роста/падения количества новых пользователей
## 📞 Поддержка
При возникновении проблем проверьте:
1. Права доступа к таблице `bot_users`
2. Корректность переменных окружения
3. Логи приложения для ошибок базы данных
Все операции с базой данных логируются для отладки.

72
create_users_table.sql Normal file
View File

@ -0,0 +1,72 @@
-- SQL скрипт для создания таблицы пользователей
-- Сохраняет все данные о пользователе, передаваемые Telegram через aiogram
CREATE TABLE bot_users (
-- Основные данные пользователя Telegram
id NUMBER(19) PRIMARY KEY, -- Telegram user ID (64-bit integer)
first_name VARCHAR2(256) NOT NULL, -- Имя пользователя
last_name VARCHAR2(256), -- Фамилия пользователя (опционально)
username VARCHAR2(128), -- Username (опционально)
language_code VARCHAR2(10), -- Код языка (IETF language tag)
-- Флаги статуса пользователя
is_bot NUMBER(1) DEFAULT 0 NOT NULL, -- Является ли пользователь ботом
is_premium NUMBER(1) DEFAULT 0, -- Telegram Premium пользователь
added_to_attachment_menu NUMBER(1) DEFAULT 0, -- Добавил ли бота в меню вложений
-- Системные поля для аудита
first_interaction_date DATE DEFAULT SYSDATE NOT NULL, -- Дата первого взаимодействия
last_interaction_date DATE DEFAULT SYSDATE NOT NULL, -- Дата последнего взаимодействия
interaction_count NUMBER(10) DEFAULT 1 NOT NULL, -- Количество взаимодействий
-- Дополнительные бизнес-поля
is_active NUMBER(1) DEFAULT 1 NOT NULL, -- Активен ли пользователь
is_blocked NUMBER(1) DEFAULT 0 NOT NULL, -- Заблокирован ли пользователь
registration_source VARCHAR2(50) DEFAULT 'bot', -- Источник регистрации
-- Финансовые данные
total_payments NUMBER(10,2) DEFAULT 0, -- Общая сумма платежей в звездах
successful_payments_count NUMBER(8) DEFAULT 0, -- Количество успешных платежей
-- Системные поля
created_at DATE DEFAULT SYSDATE NOT NULL,
updated_at DATE DEFAULT SYSDATE NOT NULL
);
-- Создание индексов для оптимизации поиска
CREATE INDEX idx_bot_users_username ON bot_users(username);
CREATE INDEX idx_bot_users_language ON bot_users(language_code);
CREATE INDEX idx_bot_users_last_interaction ON bot_users(last_interaction_date);
CREATE INDEX idx_bot_users_is_premium ON bot_users(is_premium);
CREATE INDEX idx_bot_users_created_at ON bot_users(created_at);
-- Создание комментариев к таблице и полям
COMMENT ON TABLE bot_users IS 'Таблица пользователей Telegram бота с полными данными KYC';
COMMENT ON COLUMN bot_users.id IS 'Уникальный идентификатор пользователя Telegram (64-bit)';
COMMENT ON COLUMN bot_users.first_name IS 'Имя пользователя в Telegram';
COMMENT ON COLUMN bot_users.last_name IS 'Фамилия пользователя в Telegram (опционально)';
COMMENT ON COLUMN bot_users.username IS 'Username пользователя в Telegram (опционально)';
COMMENT ON COLUMN bot_users.language_code IS 'Код языка пользователя (IETF language tag)';
COMMENT ON COLUMN bot_users.is_bot IS 'Флаг: является ли пользователь ботом (0/1)';
COMMENT ON COLUMN bot_users.is_premium IS 'Флаг: Telegram Premium пользователь (0/1)';
COMMENT ON COLUMN bot_users.added_to_attachment_menu IS 'Флаг: добавил ли бота в меню вложений (0/1)';
COMMENT ON COLUMN bot_users.first_interaction_date IS 'Дата первого взаимодействия с ботом';
COMMENT ON COLUMN bot_users.last_interaction_date IS 'Дата последнего взаимодействия с ботом';
COMMENT ON COLUMN bot_users.interaction_count IS 'Общее количество взаимодействий с ботом';
COMMENT ON COLUMN bot_users.is_active IS 'Флаг: активен ли пользователь (0/1)';
COMMENT ON COLUMN bot_users.is_blocked IS 'Флаг: заблокирован ли пользователь (0/1)';
COMMENT ON COLUMN bot_users.registration_source IS 'Источник регистрации пользователя';
COMMENT ON COLUMN bot_users.total_payments IS 'Общая сумма платежей пользователя в Telegram Stars';
COMMENT ON COLUMN bot_users.successful_payments_count IS 'Количество успешных платежей';
-- Создание триггера для автоматического обновления updated_at
CREATE OR REPLACE TRIGGER trg_bot_users_updated_at
BEFORE UPDATE ON bot_users
FOR EACH ROW
BEGIN
:NEW.updated_at := SYSDATE;
END;
/
-- Создание последовательности для системных нужд (если потребуется)
CREATE SEQUENCE seq_bot_users_internal START WITH 1 INCREMENT BY 1 NOCACHE;

193
db.py
View File

@ -1,7 +1,8 @@
# db.py # db.py
import oracledb import oracledb
from typing import Optional, Tuple from typing import Optional, Tuple
import logging # import logging
from aiogram.types import User
class OracleDatabase: class OracleDatabase:
@ -122,3 +123,193 @@ class OracleDatabase:
return detailed_info return detailed_info
import asyncio import asyncio
return await asyncio.to_thread(_query) return await asyncio.to_thread(_query)
async def save_user(self, user: User, interaction_source: str = "bot") -> bool:
"""
Сохраняет или обновляет данные пользователя в базе данных
При первом взаимодействии создает запись, при последующих - обновляет
"""
def _save_user():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
# Проверяем, существует ли пользователь
cur.execute("SELECT id FROM bot_users WHERE id = :user_id", {"user_id": user.id})
existing_user = cur.fetchone()
if existing_user:
# Обновляем существующего пользователя
update_query = """
UPDATE bot_users SET
first_name = :first_name,
last_name = :last_name,
username = :username,
language_code = :language_code,
is_premium = :is_premium,
added_to_attachment_menu = :added_to_attachment_menu,
last_interaction_date = SYSDATE,
interaction_count = interaction_count + 1
WHERE id = :user_id
"""
params = {
"user_id": user.id,
"first_name": user.first_name,
"last_name": user.last_name,
"username": user.username,
"language_code": user.language_code,
"is_premium": 1 if user.is_premium else 0,
"added_to_attachment_menu": 1 if user.added_to_attachment_menu else 0
}
else:
# Создаем нового пользователя
insert_query = """
INSERT INTO bot_users (
id, first_name, last_name, username, language_code,
is_bot, is_premium, added_to_attachment_menu,
registration_source, first_interaction_date,
last_interaction_date, interaction_count
) VALUES (
:user_id, :first_name, :last_name, :username, :language_code,
:is_bot, :is_premium, :added_to_attachment_menu,
:registration_source, SYSDATE, SYSDATE, 1
)
"""
params = {
"user_id": user.id,
"first_name": user.first_name,
"last_name": user.last_name,
"username": user.username,
"language_code": user.language_code,
"is_bot": 1 if user.is_bot else 0,
"is_premium": 1 if user.is_premium else 0,
"added_to_attachment_menu": 1 if user.added_to_attachment_menu else 0,
"registration_source": interaction_source
}
cur.execute(insert_query, params)
conn.commit()
return True
cur.execute(update_query, params)
conn.commit()
return True
try:
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _save_user)
except Exception as e:
print(f"Error saving user {user.id}: {e}")
return False
async def update_user_payment(self, user_id: int, payment_amount: float) -> bool:
"""
Обновляет данные о платежах пользователя
"""
def _update_payment():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
update_query = """
UPDATE bot_users SET
total_payments = total_payments + :amount,
successful_payments_count = successful_payments_count + 1,
last_interaction_date = SYSDATE
WHERE id = :user_id
"""
cur.execute(update_query, {
"user_id": user_id,
"amount": payment_amount
})
conn.commit()
return cur.rowcount > 0
try:
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _update_payment)
except Exception as e:
print(f"Error updating payment for user {user_id}: {e}")
return False
async def get_user_stats(self, user_id: int) -> Optional[dict]:
"""
Получает статистику пользователя из базы данных
"""
def _get_stats():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
query = """
SELECT
first_name, last_name, username, language_code,
is_premium, interaction_count, total_payments,
successful_payments_count, first_interaction_date,
last_interaction_date
FROM bot_users
WHERE id = :user_id
"""
cur.execute(query, {"user_id": user_id})
result = cur.fetchone()
if result:
return {
"first_name": result[0],
"last_name": result[1],
"username": result[2],
"language_code": result[3],
"is_premium": bool(result[4]),
"interaction_count": result[5],
"total_payments": float(result[6]) if result[6] else 0.0,
"successful_payments_count": result[7],
"first_interaction_date": result[8],
"last_interaction_date": result[9]
}
return None
try:
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _get_stats)
except Exception as e:
print(f"Error getting stats for user {user_id}: {e}")
return None
async def get_users_summary(self) -> dict:
"""
Получает общую статистику по пользователям
"""
def _get_summary():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
summary_query = """
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users,
SUM(total_payments) as total_revenue,
SUM(successful_payments_count) as total_transactions,
COUNT(CASE WHEN last_interaction_date >= SYSDATE - 1 THEN 1 END) as active_last_24h,
COUNT(CASE WHEN last_interaction_date >= SYSDATE - 7 THEN 1 END) as active_last_week
FROM bot_users
WHERE is_active = 1
"""
cur.execute(summary_query)
result = cur.fetchone()
return {
"total_users": result[0] or 0,
"premium_users": result[1] or 0,
"total_revenue": float(result[2]) if result[2] else 0.0,
"total_transactions": result[3] or 0,
"active_last_24h": result[4] or 0,
"active_last_week": result[5] or 0
}
try:
import asyncio
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _get_summary)
except Exception as e:
print(f"Error getting users summary: {e}")
return {}

76
main.py
View File

@ -1,7 +1,8 @@
import asyncio import asyncio
from os import getenv from os import getenv
import logging import logging
# import json from datetime import datetime
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery
@ -37,7 +38,10 @@ class VinStates(StatesGroup):
# Command handler # Command handler
@dp.message(Command("start")) @dp.message(Command("start"))
async def command_start_handler(message: Message) -> None: async def command_start_handler(message: Message, db: OracleDatabase) -> None:
# Сохраняем данные пользователя при каждом взаимодействии
await db.save_user(message.from_user, "start_command")
welcome_text = ( welcome_text = (
"Welcome to SalvagedbBot — your trusted assistant for vehicle history checks via VIN!\n\n" "Welcome to SalvagedbBot — your trusted assistant for vehicle history checks via VIN!\n\n"
"🔍 What You Can Discover:\n\n" "🔍 What You Can Discover:\n\n"
@ -60,26 +64,34 @@ async def command_start_handler(message: Message) -> None:
@dp.callback_query(lambda c: c.data == "decode_vin") @dp.callback_query(lambda c: c.data == "decode_vin")
async def decode_vin_callback(callback: CallbackQuery, state: FSMContext): async def decode_vin_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase):
# Сохраняем данные пользователя при нажатии кнопки
await db.save_user(callback.from_user, "decode_vin_button")
await callback.message.answer("Please enter the vehicle VIN.") await callback.message.answer("Please enter the vehicle VIN.")
await state.set_state(VinStates.waiting_for_vin) await state.set_state(VinStates.waiting_for_vin)
await callback.answer() await callback.answer()
@dp.callback_query(lambda c: c.data == "main_menu") @dp.callback_query(lambda c: c.data == "main_menu")
async def main_menu_callback(callback: CallbackQuery, state: FSMContext): async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase):
# Сохраняем данные пользователя при возврате в главное меню
await db.save_user(callback.from_user, "main_menu_button")
await state.clear() await state.clear()
await command_start_handler(callback.message) await command_start_handler(callback.message, db)
await callback.answer() await callback.answer()
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:")) @dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
async def pay_detailed_info_callback(callback: CallbackQuery): async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase):
# Сохраняем данные пользователя при инициации платежа
await db.save_user(callback.from_user, "payment_initiation")
# Extract VIN from callback data # Extract VIN from callback data
vin = callback.data.split(":")[1] vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed VIN Report", amount=1)] prices = [LabeledPrice(label="Detailed VIN Report", amount=1)]
logging.info(f"Sending invoice for VIN: {vin}")
await callback.bot.send_invoice( await callback.bot.send_invoice(
chat_id=callback.message.chat.id, chat_id=callback.message.chat.id,
title="Detailed VIN Information", title="Detailed VIN Information",
@ -94,6 +106,9 @@ async def pay_detailed_info_callback(callback: CallbackQuery):
@dp.message(VinStates.waiting_for_vin) @dp.message(VinStates.waiting_for_vin)
async def process_vin(message: Message, state: FSMContext, db: OracleDatabase): async def process_vin(message: Message, state: FSMContext, db: OracleDatabase):
# Сохраняем данные пользователя при обработке VIN
await db.save_user(message.from_user, "vin_processing")
vin = message.text.strip().upper() vin = message.text.strip().upper()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]): if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try: try:
@ -129,8 +144,51 @@ async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery):
await pre_checkout_query.answer(ok=True) await pre_checkout_query.answer(ok=True)
ADMIN_USER_ID = int(getenv("ADMIN_USER_ID", "0")) # ID администратора из переменных окружения
@dp.message(Command("admin_stats"))
async def admin_stats_handler(message: Message, db: OracleDatabase):
# Проверяем, является ли пользователь администратором
if message.from_user.id != ADMIN_USER_ID:
await message.answer("❌ Access denied. This command is for administrators only.")
return
try:
# Получаем общую статистику
stats = await db.get_users_summary()
# Формируем отчет
report = f"""
📊 **Bot Users Statistics**
👥 **Users Overview:**
Total users: {stats.get('total_users', 0)}
Premium users: {stats.get('premium_users', 0)}
💰 **Revenue:**
Total revenue: {stats.get('total_revenue', 0)}
Total transactions: {stats.get('total_transactions', 0)}
📈 **Activity:**
Active last 24h: {stats.get('active_last_24h', 0)}
Active last week: {stats.get('active_last_week', 0)}
📅 **Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
await message.answer(report, parse_mode="Markdown")
except Exception as e:
logging.error(f"Error generating admin stats: {e}")
await message.answer("❌ Error generating statistics. Please try again later.")
@dp.message(lambda message: message.successful_payment) @dp.message(lambda message: message.successful_payment)
async def successful_payment_handler(message: Message, db: OracleDatabase): async def successful_payment_handler(message: Message, db: OracleDatabase):
# Сохраняем данные о платеже пользователя
await db.save_user(message.from_user, "successful_payment")
await db.update_user_payment(message.from_user.id, 1.0) # 1 Telegram Star
payload = message.successful_payment.invoice_payload payload = message.successful_payment.invoice_payload
if payload.startswith("detailed_vin_info:"): if payload.startswith("detailed_vin_info:"):
@ -231,7 +289,7 @@ async def successful_payment_handler(message: Message, db: OracleDatabase):
report += "⚠️ **TECHNICAL INFORMATION AND ERRORS**\n" report += "⚠️ **TECHNICAL INFORMATION AND ERRORS**\n"
for key, data in detailed_info['technical_information_and_errors'].items(): for key, data in detailed_info['technical_information_and_errors'].items():
if data['value'] == "0": if data['value'] == "0":
report += f"✅ **No errors found**\n" report += "✅ **No errors found**\n"
else: else:
report += f"• **{data['param_name']}:** {data['value']}\n" report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n" report += "\n"