- app/services/ym.py: клиент Partner API — getGoodsFeedbacks с пагинацией и фильтром по дате, updateGoodsFeedbackComment (ответ), deleteGoodsFeedbackComment (удаление), обработка новых отзывов по аналогии с WB/Ozon - воркер ym_worker, включается при заданных YM_API_KEY и YM_BUSINESS_ID - кнопки отправки/удаления ответа, платформа в сообщении и в таблице, статистика /stat и /allstat - переменные YM_API_KEY, YM_BUSINESS_ID, YM_CHECK_INTERVAL, PRODUCTS_YM_SHEET_LINK - README и .env.example Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
160 lines
6.8 KiB
Python
160 lines
6.8 KiB
Python
import logging
|
||
|
||
from aiogram import Router
|
||
from aiogram.filters import Command
|
||
from aiogram.types import Message
|
||
|
||
from app import config
|
||
from app.handlers.callbacks import _settings_keyboard
|
||
from app.models import Settings
|
||
from app.services import StatsService
|
||
|
||
router = Router()
|
||
logger = logging.getLogger(__name__)
|
||
|
||
stats_service = StatsService()
|
||
|
||
|
||
@router.message(Command("allstat", "stat"))
|
||
async def cmd_stats(message: Message) -> None:
|
||
is_all_time = message.text.startswith("/allstat")
|
||
ozon_stats = (
|
||
await stats_service.get_all_time_stats("ozon")
|
||
if is_all_time
|
||
else await stats_service.get_daily_stats("ozon")
|
||
)
|
||
wb_stats = (
|
||
await stats_service.get_all_time_stats("wb")
|
||
if is_all_time
|
||
else await stats_service.get_daily_stats("wb")
|
||
)
|
||
ym_stats = (
|
||
await stats_service.get_all_time_stats("ym")
|
||
if is_all_time
|
||
else await stats_service.get_daily_stats("ym")
|
||
)
|
||
|
||
period_text = "за все время" if is_all_time else "за сегодня"
|
||
|
||
stats_text = f"📊 <b>Статистика {period_text}</b>\n\n"
|
||
|
||
for platform_name, stats in [
|
||
("Ozon", ozon_stats),
|
||
("Wildberries", wb_stats),
|
||
("Яндекс Маркет", ym_stats),
|
||
]:
|
||
stats_text += (
|
||
f"<b>{platform_name}:</b>\n"
|
||
f"Всего отзывов: {stats['total_reviews']}\n"
|
||
f"Хороших: {stats['good_reviews']}\n"
|
||
f"Плохих: {stats['bad_reviews']}\n"
|
||
f"Доля плохих: {stats['bad_ratio']:.1%}\n"
|
||
f"Средний рейтинг: {stats['average_rating']:.1f}\n"
|
||
f"Критичных: {stats['critical_reviews']} ({stats['critical_ratio']:.1%} от всех, {stats['critical_to_bad_ratio']:.1%} от плохих)\n\n"
|
||
f"<b>Оценки:</b>\n"
|
||
f"⭐️ 1: {stats['ratings']['1']}\n"
|
||
f"⭐️ 2: {stats['ratings']['2']}\n"
|
||
f"⭐️ 3: {stats['ratings']['3']}\n"
|
||
f"⭐️ 4: {stats['ratings']['4']}\n"
|
||
f"⭐️ 5: {stats['ratings']['5']}\n\n"
|
||
f"<b>Тональность:</b>\n"
|
||
f"😊 Позитивная: {stats['tone']['positive']}\n"
|
||
f"😐 Нейтральная: {stats['tone']['neutral']}\n"
|
||
f"😠 Негативная: {stats['tone']['negative']}\n\n"
|
||
)
|
||
|
||
await message.answer(stats_text, parse_mode="HTML")
|
||
|
||
|
||
@router.message(Command("settings"))
|
||
async def cmd_settings(message: Message) -> None:
|
||
settings = await Settings.get_from_context(
|
||
message_thread_id=message.message_thread_id
|
||
)
|
||
|
||
await message.answer(
|
||
(
|
||
f"⚙️ <b>Настройки {'положительных' if settings.is_good else 'отрицательных'} отзывов</b>\n\n"
|
||
"/show_prompt - просмотр промптов\n"
|
||
"/change_prompt - изменить промпт для аналитики\n"
|
||
"/reset_prompt - сбросить промпт для аналитики\n"
|
||
"/change_answer_prompt - изменить промпт для ответов на отзывы\n"
|
||
"/reset_answer_prompt - сбросить промпт для ответов на отзывы\n\n"
|
||
"<b>Шаблоны для отзывов без текста:</b>\n"
|
||
"/template_low текст — шаблон для 1–3 ★\n"
|
||
"/template_high текст — шаблон для 4–5 ★\n"
|
||
"/show_templates — показать текущие шаблоны\n"
|
||
),
|
||
reply_markup=_settings_keyboard(settings),
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
|
||
@router.message(Command("show_templates"))
|
||
async def cmd_show_templates(message: Message) -> None:
|
||
"""Показать текущие шаблоны ответа на отзывы без текста."""
|
||
settings = await Settings.get_from_context(
|
||
message_thread_id=message.message_thread_id
|
||
)
|
||
low = settings.template_empty_low_rating or config.TEMPLATE_EMPTY_LOW_RATING
|
||
high = settings.template_empty_high_rating or config.TEMPLATE_EMPTY_HIGH_RATING
|
||
await message.answer(
|
||
"📝 <b>Шаблоны для отзывов без текста</b>\n\n"
|
||
f"<b>1–3 ★:</b> <code>{low}</code>\n\n"
|
||
f"<b>4–5 ★:</b> <code>{high}</code>\n\n"
|
||
"Изменить: /template_low текст или /template_high текст",
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
|
||
@router.message(Command("template_low"))
|
||
async def cmd_template_low(message: Message) -> None:
|
||
"""Установить шаблон ответа для отзывов без текста с оценкой 1–3 звезды."""
|
||
text = (message.text or "").replace("/template_low", "").strip()
|
||
if not text:
|
||
await message.answer(
|
||
"Введите текст шаблона после команды.\n"
|
||
"Пример: /template_low Спасибо за отзыв, извините за неудобства."
|
||
)
|
||
return
|
||
for s in await Settings.all():
|
||
s.template_empty_low_rating = text
|
||
await s.save()
|
||
await message.answer(f"✅ Шаблон для 1–3 ★ сохранён:\n<code>{text}</code>", parse_mode="HTML")
|
||
|
||
|
||
@router.message(Command("template_high"))
|
||
async def cmd_template_high(message: Message) -> None:
|
||
"""Установить шаблон ответа для отзывов без текста с оценкой 4–5 звёзд."""
|
||
text = (message.text or "").replace("/template_high", "").strip()
|
||
if not text:
|
||
await message.answer(
|
||
"Введите текст шаблона после команды.\n"
|
||
"Пример: /template_high Спасибо за отзыв, рады что вам понравилось!"
|
||
)
|
||
return
|
||
for s in await Settings.all():
|
||
s.template_empty_high_rating = text
|
||
await s.save()
|
||
await message.answer(f"✅ Шаблон для 4–5 ★ сохранён:\n<code>{text}</code>", parse_mode="HTML")
|
||
|
||
|
||
@router.message(Command("max_tokens"))
|
||
async def cmd_max_tokens(message: Message) -> None:
|
||
settings = await Settings.get_from_context(
|
||
message_thread_id=message.message_thread_id
|
||
)
|
||
|
||
args = message.text.split()
|
||
if len(args) > 1 and args[1].isdigit():
|
||
value = int(args[1])
|
||
settings.max_tokens = value
|
||
await settings.save()
|
||
await message.answer(f"Лимит токенов установлен на {value}")
|
||
return
|
||
|
||
await message.answer(
|
||
f"Текущий лимит токенов: {settings.max_tokens}\n"
|
||
"Введите /max_tokens число, чтобы обновить его"
|
||
)
|