Добавлены функции для сохранения и обновления данных пользователей в классе 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
import oracledb
from typing import Optional, Tuple
import logging
# import logging
from aiogram.types import User
class OracleDatabase:
@ -122,3 +123,193 @@ class OracleDatabase:
return detailed_info
import asyncio
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
from os import getenv
import logging
# import json
from datetime import datetime
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery
@ -37,7 +38,10 @@ class VinStates(StatesGroup):
# Command handler
@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 to SalvagedbBot — your trusted assistant for vehicle history checks via VIN!\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")
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 state.set_state(VinStates.waiting_for_vin)
await callback.answer()
@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 command_start_handler(callback.message)
await command_start_handler(callback.message, db)
await callback.answer()
@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
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed VIN Report", amount=1)]
logging.info(f"Sending invoice for VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed VIN Information",
@ -94,6 +106,9 @@ async def pay_detailed_info_callback(callback: CallbackQuery):
@dp.message(VinStates.waiting_for_vin)
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()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try:
@ -129,8 +144,51 @@ async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery):
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)
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
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"
for key, data in detailed_info['technical_information_and_errors'].items():
if data['value'] == "0":
report += f"✅ **No errors found**\n"
report += "✅ **No errors found**\n"
else:
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"