Добавить поддержку Яндекс Маркета (goods-feedback API)

- 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>
This commit is contained in:
2026-09-04 20:13:38 +03:00
co-authored by Claude Fable 5.1
parent 1098ff4991
commit 4534ab2683
10 changed files with 481 additions and 24 deletions
+56 -1
View File
@@ -10,7 +10,7 @@ from tortoise.exceptions import DoesNotExist
from app import config
from app.models import Review, Settings
from app.services import GPTClient, OzonClient, WildberriesClient
from app.services import GPTClient, OzonClient, WildberriesClient, YandexMarketClient
logger = logging.getLogger(__name__)
router = Router()
@@ -147,6 +147,33 @@ async def callback_wb_delete(call: CallbackQuery) -> None:
)
@router.callback_query(F.data.startswith("ym_delete:"))
async def callback_ym_delete(call: CallbackQuery) -> None:
comment_id = call.data.split(":", 1)[1]
ym_client = YandexMarketClient()
result = await ym_client.delete_comment(comment_id)
if "error" not in result:
try:
review = await Review.filter(response_id=comment_id, platform="ym").first()
if review:
review.response_text = None
review.response_id = None
review.responded_at = None
await review.save()
except Exception as e:
logger.error(f"Error updating review after comment deletion: {e}")
await call.answer("Комментарий успешно удалён на Яндекс Маркете.")
await call.message.reply("🗑 Ответ успешно удален с Яндекс Маркета")
else:
await call.answer("Не удалось удалить комментарий.")
await call.message.reply(
f"❌ Ошибка удаления комментария: {result.get('error')}"
)
@router.callback_query(F.data.startswith("generate_answer:"))
async def callback_generate_answer(call: CallbackQuery) -> None:
review_id = call.data.split(":", 1)[1]
@@ -344,6 +371,34 @@ async def callback_send_answer(call: CallbackQuery) -> None:
f"❌ Ошибка отправки ответа на Wildberries: {result.get('error')}"
)
elif review.platform == "ym":
ym_client = YandexMarketClient()
result = await ym_client.post_comment(
review.external_id, review.response_text
)
if "comment_id" in result:
review.response_id = result["comment_id"]
await review.save()
markup = InlineKeyboardBuilder()
markup.add(
InlineKeyboardButton(
text="Удалить",
callback_data=f"ym_delete:{result['comment_id']}",
)
)
await call.message.answer(
f"✅ Ответ на #review{review_id} успешно отправлен на Яндекс Маркет",
reply_to_message_id=review.message_id,
reply_markup=markup.as_markup(),
)
else:
await call.message.reply(
f"❌ Ошибка отправки ответа на Яндекс Маркет: {result.get('error')}"
)
await call.answer("Ответ отправлен")
review.responded_at = datetime.now()
await review.save()
+10 -1
View File
@@ -28,12 +28,21 @@ async def cmd_stats(message: Message) -> None:
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)]:
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"
+9 -2
View File
@@ -2,6 +2,7 @@ import asyncio
import html
import logging
from asyncio import Queue
from urllib.parse import quote
from datetime import datetime
from aiogram import Router, types
@@ -15,6 +16,9 @@ from app.services.sheets import add_rows_to_sheet
logger = logging.getLogger(__name__)
router = Router()
PLATFORM_NAMES = {"ozon": "Ozon", "wb": "Wildberries", "ym": "Яндекс Маркет"}
PLATFORM_TAGS = {"ozon": "🟦 #Ozon", "wb": "🟪 #Wildberries", "ym": "🟨 #ЯндексМаркет"}
send_queue = Queue()
sheet_queue = Queue()
@@ -52,7 +56,7 @@ def render_review_to_sheet(review: Review) -> list[str]:
# Порядок столбцов таблицы (12 колонок): 1–8 данные, 9 тональность, 10 причина, 11 критичность, 12 Тэги
return [
formatted_date,
"Ozon" if review.platform == "ozon" else "Wildberries",
PLATFORM_NAMES.get(review.platform, review.platform),
review.product_name or "-",
review.product_category or "-",
review.product_subcategory or "-",
@@ -192,6 +196,9 @@ async def send_review(review: Review) -> None:
)
if review.platform == "ozon":
product_url = f"https://www.ozon.ru/product/{review.product_id}/"
elif review.platform == "ym":
# У Маркета нет публичной ссылки по SKU продавца — ведём в поиск по нему
product_url = f"https://market.yandex.ru/search?text={quote(review.product_id)}"
else:
product_url = (
f"https://www.wildberries.ru/catalog/{review.product_id}/detail.aspx"
@@ -219,7 +226,7 @@ async def send_review(review: Review) -> None:
+ f"{'✅ Положительный' if review.is_good else '⚠️ Отрицательный'} отзыв №{count + 1}\n"
f"📅 Дата: {formatted_date}\n"
f"📦 Товар: <code>{escaped_product_id}</code> #{category_hashtag}\n"
f"{'🟪 #Wildberries' if review.platform == 'wb' else '🟦 #Ozon'}\n"
f"{PLATFORM_TAGS.get(review.platform, '#' + review.platform)}\n"
f"🔗 <a href='{product_url}'>Ссылка</a>\n"
f"⭐ Оценка: <code>{review.rating}/5</code>\n"
f"💬 Отзыв: {escaped_text}\n\n"