Первый коммит с .gitignore
This commit is contained in:
commit
9a3639b5ff
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
TG_BOT_TOKEN=7727936968:AAELs0gg5VNGzg6wMnG66CkpYaEv0K1Tp5w
|
||||||
|
API_URL=http://127.0.0.1:8000
|
||||||
119
.gitignore
vendored
Normal file
119
.gitignore
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# SQLite database
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Other
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
62
README.md
Normal file
62
README.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Главный экран
|
||||||
|
Появляется после ввода команды /start
|
||||||
|
|
||||||
|
Текст сообщения: Официальное приветствие. Коротко описание что это бот для партнерки
|
||||||
|
|
||||||
|
Кнопки:
|
||||||
|
1. Мои ссылки — просмотр ваших партнерских ссылок с пагинацией
|
||||||
|
2. Промо материалы — переход по внешней ссылке
|
||||||
|
3. Партнерское соглашение — переход по внешней ссылке
|
||||||
|
4. Задать вопрос — всплывающее сообщение (функция не реализована)
|
||||||
|
5. Моя статистика — просмотр общей статистики и статистики по рефералам с пагинацией
|
||||||
|
6. Создать заявку на вывод средств — всплывающее сообщение (функция не реализована)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Формат файлов данных
|
||||||
|
|
||||||
|
### partner-tg/my_links_mock_data.json
|
||||||
|
Массив объектов:
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"link": "stores-apple.com?ref=<uuid>", // ссылка с уникальным идентификатором
|
||||||
|
"name": "Название ссылки" // описание/название ссылки
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `link` — уникальная партнерская ссылка
|
||||||
|
- `name` — описание/название ссылки
|
||||||
|
|
||||||
|
### partner-tg/my_ststs_mock_data.json
|
||||||
|
Объект:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"totalSales": int, // общее количество продаж
|
||||||
|
"totalIncome": float, // общий доход
|
||||||
|
"availableWithdrawal": float, // доступно к выводу
|
||||||
|
"refData": [ // список данных по рефералам
|
||||||
|
{
|
||||||
|
"name": str, // название реферальной ссылки
|
||||||
|
"sales": int, // количество продаж по ссылке
|
||||||
|
"income": float // доход по ссылке
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `totalSales` — общее количество продаж
|
||||||
|
- `totalIncome` — общий доход
|
||||||
|
- `availableWithdrawal` — сумма, доступная к выводу
|
||||||
|
- `refData` — массив статистики по каждой реферальной ссылке
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Основные функции бота
|
||||||
|
- Все экраны реализованы через редактирование исходного сообщения (edit_text), а не отправку новых сообщений.
|
||||||
|
- Для списков и таблиц реализована пагинация (по 10 элементов на страницу).
|
||||||
|
- Кнопки навигации и возврата всегда присутствуют на соответствующих экранах.
|
||||||
|
- Для не реализованных функций выводится всплывающее сообщение (show_alert).
|
||||||
355
main.py
Normal file
355
main.py
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from aiogram import Bot, Dispatcher, types, F
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.filters import CommandStart
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import json
|
||||||
|
import aiohttp
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
API_TOKEN = os.getenv('TG_BOT_TOKEN')
|
||||||
|
API_URL = os.getenv('API_URL')
|
||||||
|
|
||||||
|
|
||||||
|
bot = Bot(token=API_TOKEN)
|
||||||
|
dp = Dispatcher()
|
||||||
|
storage = MemoryStorage()
|
||||||
|
dp.fsm.storage = storage
|
||||||
|
|
||||||
|
# Текст приветствия для главного экрана
|
||||||
|
WELCOME_TEXT = (
|
||||||
|
'👋 Добро пожаловать!\n\n'
|
||||||
|
'Это официальный бот для партнерской программы.\n'
|
||||||
|
'Здесь вы найдете свои партнерские ссылки, промо материалы, статистику и сможете задать вопросы.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Количество ссылок/рефералов на одной странице (для пагинации)
|
||||||
|
LINKS_PER_PAGE = 10
|
||||||
|
PROMO_URL = 'https://telegra.ph/Test-05-24-363'
|
||||||
|
PARTNER_AGREEMENT_URL = 'https://telegra.ph/Test-05-24-363'
|
||||||
|
|
||||||
|
# Клавиатура главного экрана
|
||||||
|
main_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text='Мои ссылки', callback_data='links'),
|
||||||
|
InlineKeyboardButton(text='Промо материалы', url=PROMO_URL)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text='Партнерское соглашение', url=PARTNER_AGREEMENT_URL),
|
||||||
|
InlineKeyboardButton(text='Задать вопрос', callback_data='question')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text='Моя статистика', callback_data='stats'),
|
||||||
|
InlineKeyboardButton(text='Создать заявку на вывод средств', callback_data='withdraw')
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
# Вместо загрузки из файла будем хранить кэш токенов пользователей
|
||||||
|
user_tokens = {}
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Формат my_links_mock_data.json:
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "ref": "<uuid>", # ссылка с уникальным идентификатором
|
||||||
|
# "description": "Описание/название ссылки" # описание/название ссылки
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
async def get_links_from_api(token: str):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f'{API_URL}/ref', headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.json()
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def render_links_page(page: int = 0, token: str = None):
|
||||||
|
"""
|
||||||
|
Формирует текст и клавиатуру для страницы с партнерскими ссылками.
|
||||||
|
Получает данные с API.
|
||||||
|
"""
|
||||||
|
links = await get_links_from_api(token)
|
||||||
|
total = len(links)
|
||||||
|
start = page * LINKS_PER_PAGE
|
||||||
|
end = start + LINKS_PER_PAGE
|
||||||
|
page_links = links[start:end]
|
||||||
|
header = f"{'ref':<36} | description"
|
||||||
|
sep = '-' * 36 + '-|-' + '-' * 30
|
||||||
|
rows = []
|
||||||
|
for item in page_links:
|
||||||
|
ref = item['ref']
|
||||||
|
description = item['description']
|
||||||
|
url = f"https://{item['ref']}"
|
||||||
|
rows.append(f"{ref:<36} | <a href=\"{url}\">{description}</a>")
|
||||||
|
if not rows:
|
||||||
|
table = 'Нет ссылок.'
|
||||||
|
else:
|
||||||
|
table = '\n'.join([header, sep] + rows)
|
||||||
|
text = f"🔗 Ваши партнерские ссылки (стр. {page+1}):\n\n<pre>{table}</pre>"
|
||||||
|
nav_buttons = []
|
||||||
|
if start > 0:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text='⬅️ Предыдущая', callback_data=f'links_page_{page-1}'))
|
||||||
|
if end < total:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text='Следующая ➡️', callback_data=f'links_page_{page+1}'))
|
||||||
|
keyboard_rows = []
|
||||||
|
if nav_buttons:
|
||||||
|
keyboard_rows.append(nav_buttons)
|
||||||
|
keyboard_rows.append([InlineKeyboardButton(text='Создать новую', callback_data='create_link')])
|
||||||
|
keyboard_rows.append([InlineKeyboardButton(text='Назад', callback_data='back_to_main')])
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
|
||||||
|
return text, keyboard
|
||||||
|
|
||||||
|
@dp.message(CommandStart())
|
||||||
|
async def send_welcome(message: types.Message):
|
||||||
|
"""Обрабатывает команду /start, авторизует пользователя и показывает главный экран."""
|
||||||
|
tg_id = message.from_user.id
|
||||||
|
chat_id = message.chat.id
|
||||||
|
name = message.from_user.full_name
|
||||||
|
login = message.from_user.username
|
||||||
|
token = None
|
||||||
|
error = None
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# 1. Пробуем получить токен
|
||||||
|
try:
|
||||||
|
async with session.post(f'{API_URL}/token', json={'tg_id': tg_id}) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
token = data.get('access_token')
|
||||||
|
else:
|
||||||
|
# 2. Если не ок — регистрируем
|
||||||
|
async with session.post(f'{API_URL}/register', json={'tg_id': tg_id, 'chat_id': chat_id, 'name': name, 'login': login}) as reg_resp:
|
||||||
|
if reg_resp.status == 200:
|
||||||
|
# 3. Пробуем снова получить токен
|
||||||
|
async with session.post(f'{API_URL}/token', json={'tg_id': tg_id}) as resp2:
|
||||||
|
if resp2.status == 200:
|
||||||
|
data = await resp2.json()
|
||||||
|
token = data.get('access_token')
|
||||||
|
else:
|
||||||
|
error = 'Ошибка авторизации (token после регистрации)'
|
||||||
|
else:
|
||||||
|
error = 'Ошибка регистрации пользователя'
|
||||||
|
except Exception as e:
|
||||||
|
error = f'Ошибка соединения с сервером авторизации: {e}'
|
||||||
|
if token:
|
||||||
|
user_tokens[tg_id] = token
|
||||||
|
await message.answer(WELCOME_TEXT, reply_markup=main_keyboard)
|
||||||
|
else:
|
||||||
|
await message.answer(f'Ошибка авторизации: {error}', reply_markup=None)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'links')
|
||||||
|
async def show_links(callback: types.CallbackQuery):
|
||||||
|
"""Показывает страницу с партнерскими ссылками (первая страница)."""
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_links_page(0, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(lambda c: c.data and c.data.startswith('links_page_'))
|
||||||
|
async def paginate_links(callback: types.CallbackQuery):
|
||||||
|
"""Пагинация по партнерским ссылкам."""
|
||||||
|
page = int(callback.data.split('_')[-1])
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_links_page(page, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'back_to_main')
|
||||||
|
async def back_to_main(callback: types.CallbackQuery):
|
||||||
|
"""Возврат на главный экран."""
|
||||||
|
await callback.message.edit_text(WELCOME_TEXT, reply_markup=main_keyboard)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
# Состояния для FSM
|
||||||
|
class LinkStates(StatesGroup):
|
||||||
|
waiting_for_description = State()
|
||||||
|
link_created = State()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'create_link')
|
||||||
|
async def create_link(callback: types.CallbackQuery, state: FSMContext):
|
||||||
|
sent = await callback.message.edit_text(
|
||||||
|
'Введите описание новой ссылке:',
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[[InlineKeyboardButton(text='Назад', callback_data='back_to_links')]]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Сохраняем id сообщения, чтобы потом убрать у него кнопки
|
||||||
|
await state.set_state(LinkStates.waiting_for_description)
|
||||||
|
await state.update_data(desc_msg_id=sent.message_id)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
# Обработка кнопки "Назад" из состояния создания ссылки
|
||||||
|
@dp.callback_query(F.data == 'back_to_links', LinkStates.waiting_for_description)
|
||||||
|
async def back_to_links_from_create(callback: types.CallbackQuery, state: FSMContext):
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_links_page(0, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await state.clear()
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
# Обработка ввода описания новой ссылки
|
||||||
|
@dp.message(LinkStates.waiting_for_description)
|
||||||
|
async def process_new_link_description(message: types.Message, state: FSMContext):
|
||||||
|
description = message.text.strip()
|
||||||
|
tg_id = message.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
data = await state.get_data()
|
||||||
|
desc_msg_id = data.get('desc_msg_id')
|
||||||
|
# Убираем кнопки у сообщения с просьбой ввести описание
|
||||||
|
if desc_msg_id:
|
||||||
|
try:
|
||||||
|
await message.bot.edit_message_reply_markup(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=desc_msg_id,
|
||||||
|
reply_markup=None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(f'{API_URL}/ref/add', json={"description": description}, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
ref = data.get('ref')
|
||||||
|
text = f"Новая ссылка успешно создана!\n\n<pre>ref: {ref}\ndescription: {description}</pre>"
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[[InlineKeyboardButton(text='Назад', callback_data='back_to_links_from_success')]]
|
||||||
|
)
|
||||||
|
await message.answer(text, reply_markup=keyboard, parse_mode='HTML')
|
||||||
|
await state.set_state(LinkStates.link_created)
|
||||||
|
else:
|
||||||
|
await message.answer('Ошибка при создании ссылки. Попробуйте еще раз.')
|
||||||
|
|
||||||
|
# Обработка кнопки "Назад" после успешного создания ссылки
|
||||||
|
@dp.callback_query(F.data == 'back_to_links_from_success', LinkStates.link_created)
|
||||||
|
async def back_to_links_from_success(callback: types.CallbackQuery, state: FSMContext):
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_links_page(0, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await state.clear()
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'withdraw')
|
||||||
|
async def not_implemented_withdraw(callback: types.CallbackQuery):
|
||||||
|
await callback.answer('Функция вывода средств пока не реализована.', show_alert=True)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'question')
|
||||||
|
async def not_implemented_question(callback: types.CallbackQuery):
|
||||||
|
await callback.answer('Функция "Задать вопрос" пока не реализована.', show_alert=True)
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Формат my_ststs_mock_data.json:
|
||||||
|
# {
|
||||||
|
# "totalSales": int, # общее количество продаж
|
||||||
|
# "totalIncome": float, # общий доход
|
||||||
|
# "availableWithdrawal": float, # доступно к выводу
|
||||||
|
# "refData": [ # список данных по рефералам
|
||||||
|
# {
|
||||||
|
# "name": str, # название реферальной ссылки
|
||||||
|
# "sales": int, # количество продаж по ссылке
|
||||||
|
# "income": float # доход по ссылке
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
async def get_stat_from_api(token: str):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f'{API_URL}/stat', headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.json()
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_ref_stat_from_api(token: str):
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f'{API_URL}/ref/stat', headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
return data.get('refData', [])
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def render_stats_page(page: int = 0, token: str = None):
|
||||||
|
"""
|
||||||
|
Формирует текст и клавиатуру для страницы со статистикой.
|
||||||
|
Показывает общую статистику и таблицу по рефералам (по 10 на страницу).
|
||||||
|
"""
|
||||||
|
stats = await get_stat_from_api(token)
|
||||||
|
ref_data = await get_ref_stat_from_api(token)
|
||||||
|
total = len(ref_data)
|
||||||
|
start = page * LINKS_PER_PAGE
|
||||||
|
end = start + LINKS_PER_PAGE
|
||||||
|
page_refs = ref_data[start:end]
|
||||||
|
# Общая статистика
|
||||||
|
if stats:
|
||||||
|
stat_text = (
|
||||||
|
f"<b>Ваша статистика</b>\n"
|
||||||
|
f"Всего продаж: <b>{stats.get('totalSales', 0)}</b>\n"
|
||||||
|
f"Всего заработано: <b>{stats.get('totalIncome', 0):.1f}</b>\n"
|
||||||
|
f"Доступно к выводу: <b>{stats.get('availableWithdrawal', 0):.1f}</b>\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
stat_text = "<b>Ваша статистика</b>\nОшибка получения данных."
|
||||||
|
# Таблица по рефералам
|
||||||
|
header = f"{'description':<30} | {'sales':<5} | income"
|
||||||
|
sep = '-' * 30 + '-|-' + '-' * 5 + '-|-' + '-' * 10
|
||||||
|
rows = []
|
||||||
|
for item in page_refs:
|
||||||
|
description = item.get('description', '')[:30]
|
||||||
|
sales = item.get('sales', 0)
|
||||||
|
income = item.get('income', 0)
|
||||||
|
rows.append(f"{description:<30} | {sales:<5} | {income:.1f}")
|
||||||
|
if not rows:
|
||||||
|
table = 'Нет данных.'
|
||||||
|
else:
|
||||||
|
table = '\n'.join([header, sep] + rows)
|
||||||
|
text = stat_text + '\n<pre>' + table + '</pre>'
|
||||||
|
# Кнопки навигации и перехода
|
||||||
|
nav_buttons = []
|
||||||
|
if start > 0:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text='⬅️ Предыдущая', callback_data=f'stats_page_{page-1}'))
|
||||||
|
if end < total:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text='Следующая ➡️', callback_data=f'stats_page_{page+1}'))
|
||||||
|
keyboard_rows = []
|
||||||
|
if nav_buttons:
|
||||||
|
keyboard_rows.append(nav_buttons)
|
||||||
|
keyboard_rows.append([InlineKeyboardButton(text='Мои ссылки', callback_data='links')])
|
||||||
|
keyboard_rows.append([InlineKeyboardButton(text='Назад', callback_data='back_to_main')])
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
|
||||||
|
return text, keyboard
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == 'stats')
|
||||||
|
async def show_stats(callback: types.CallbackQuery):
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_stats_page(0, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.callback_query(lambda c: c.data and c.data.startswith('stats_page_'))
|
||||||
|
async def paginate_stats(callback: types.CallbackQuery):
|
||||||
|
page = int(callback.data.split('_')[-1])
|
||||||
|
tg_id = callback.from_user.id
|
||||||
|
token = user_tokens.get(tg_id)
|
||||||
|
text, keyboard = await render_stats_page(page, token)
|
||||||
|
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML', disable_web_page_preview=True)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
17
my_links_mock_data.json
Normal file
17
my_links_mock_data.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{"ref": "7e2a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c", "description": "Реклама в Telegram от 01.05.2025"},
|
||||||
|
{"ref": "2f3e4d5c-6b7a-8c9d-0e1f-2a3b4c5d6e7f", "description": "Пост в Instagram от 15.04.2025"},
|
||||||
|
{"ref": "8c7b6a5d-4e3f-2a1b-0c9d-8e7f6a5b4c3d", "description": "Рассылка в ВК от 24.02.2025"},
|
||||||
|
{"ref": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "description": "Промо в Facebook от 10.03.2025"},
|
||||||
|
{"ref": "9d8c7b6a-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "description": "Пост в Twitter от 05.03.2025"},
|
||||||
|
{"ref": "3b2a1c0d-9e8f-7a6b-5c4d-3e2f1a0b9c8d", "description": "Реклама в YouTube от 12.04.2025"},
|
||||||
|
{"ref": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", "description": "Промо в TikTok от 20.04.2025"},
|
||||||
|
{"ref": "5c4d3e2f-1a0b-9c8d-7e6f-5a4b3c2d1e0f", "description": "Пост в LinkedIn от 18.03.2025"},
|
||||||
|
{"ref": "6a7b8c9d-0e1f-2a3b-4c5d-6e7f8a9b0c1d", "description": "Реклама в Одноклассниках от 22.03.2025"},
|
||||||
|
{"ref": "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e", "description": "Промо в Pinterest от 30.03.2025"},
|
||||||
|
{"ref": "8a9b0c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d", "description": "Пост в Reddit от 02.04.2025"},
|
||||||
|
{"ref": "9b0c1d2e-3f4a-5b6c-7d8e-9f0a1b2c3d4e", "description": "Реклама в Dzen от 11.04.2025"},
|
||||||
|
{"ref": "0c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "description": "Промо в Medium от 08.04.2025"},
|
||||||
|
{"ref": "1d2e3f4a-5b6c-7d8e-9f0a-1b2c3d4e5f6a", "description": "Пост в WhatsApp от 14.04.2025"},
|
||||||
|
{"ref": "2e3f4a5b-6c7d-8e9f-0a1b-2c3d4e5f6a7b", "description": "Реклама в Discord от 21.04.2025"}
|
||||||
|
]
|
||||||
22
my_ststs_mock_data.json
Normal file
22
my_ststs_mock_data.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"totalSales": 123,
|
||||||
|
"totalIncome": 45678.90,
|
||||||
|
"availableWithdrawal": 12345.67,
|
||||||
|
"refData": [
|
||||||
|
{"description": "Реклама в Telegram от 01.05.2025", "sales": 12, "income": 1234.56},
|
||||||
|
{"description": "Пост в Instagram от 15.04.2025", "sales": 8, "income": 987.65},
|
||||||
|
{"description": "Рассылка в ВК от 24.02.2025", "sales": 15, "income": 1500.00},
|
||||||
|
{"description": "Промо в Facebook от 10.03.2025", "sales": 5, "income": 500.00},
|
||||||
|
{"description": "Пост в Twitter от 05.03.2025", "sales": 7, "income": 700.00},
|
||||||
|
{"description": "Реклама в YouTube от 12.04.2025", "sales": 10, "income": 1100.00},
|
||||||
|
{"description": "Промо в TikTok от 20.04.2025", "sales": 6, "income": 600.00},
|
||||||
|
{"description": "Пост в LinkedIn от 18.03.2025", "sales": 4, "income": 400.00},
|
||||||
|
{"description": "Реклама в Одноклассниках от 22.03.2025", "sales": 9, "income": 900.00},
|
||||||
|
{"description": "Промо в Pinterest от 30.03.2025", "sales": 3, "income": 300.00},
|
||||||
|
{"description": "Пост в Reddit от 02.04.2025", "sales": 2, "income": 200.00},
|
||||||
|
{"description": "Реклама в Dzen от 11.04.2025", "sales": 1, "income": 100.00},
|
||||||
|
{"description": "Промо в Medium от 08.04.2025", "sales": 11, "income": 1100.00},
|
||||||
|
{"description": "Пост в WhatsApp от 14.04.2025", "sales": 13, "income": 1300.00},
|
||||||
|
{"description": "Реклама в Discord от 21.04.2025", "sales": 14, "income": 1400.00}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user