текущая версия (рабочая)
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app import config
|
||||
from app.handlers.reviews import add_to_message_queue
|
||||
from app.models import Review, Settings
|
||||
from app.services.gpt import GPTClient
|
||||
from app.services.sheets import read_sheet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_cache_key() -> int:
|
||||
return int(time.time()) // config.PRODUCTS_CACHE_TTL
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_products_dict(_: int) -> dict[str, dict[str, Any]]:
|
||||
logger.info("Requesting Ozon products list")
|
||||
sheet = read_sheet(config.PRODUCTS_OZON_SHEET_LINK)
|
||||
return {
|
||||
str(product.get("ozon id акутальный") or product.get("Ozon ID")): product
|
||||
for product in sheet
|
||||
}
|
||||
|
||||
|
||||
class OzonClient:
|
||||
def __init__(self):
|
||||
self.base_url = config.OZON_BASE_URL
|
||||
self.headers = {
|
||||
"Client-Id": config.OZON_CLIENT_ID,
|
||||
"Api-Key": config.OZON_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async def get_reviews(self, limit: int = 50) -> list[dict[str, Any]]:
|
||||
url = f"{self.base_url}/v1/review/list"
|
||||
data = {"limit": limit, "sort_dir": "DESC", "status": "ALL"}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=self.headers, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json().get("reviews", [])
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Error fetching Ozon reviews: {e}")
|
||||
return []
|
||||
|
||||
async def post_comment(
|
||||
self, review_id: str, text: str, mark_processed: bool = True
|
||||
) -> dict[str, Any]:
|
||||
url = f"{self.base_url}/v1/review/comment/create"
|
||||
data = {
|
||||
"review_id": review_id,
|
||||
"text": text,
|
||||
"mark_review_as_processed": mark_processed,
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=self.headers, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Error posting comment to Ozon: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def delete_comment(self, comment_id: str) -> dict[str, Any]:
|
||||
url = f"{self.base_url}/v1/review/comment/delete"
|
||||
data = {"comment_id": comment_id}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=self.headers, json=data)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Error deleting comment from Ozon: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(iso_datetime: str) -> str:
|
||||
dt = datetime.fromisoformat(iso_datetime.replace("Z", "+00:00"))
|
||||
dt = dt.astimezone(config.TIMEZONE)
|
||||
return dt.strftime("%d.%m.%Y %H:%M:%S") + " МСК"
|
||||
|
||||
async def process_new_reviews(self) -> dict[str, int]:
|
||||
gpt_client = GPTClient()
|
||||
|
||||
try:
|
||||
latest_review = (
|
||||
await Review.filter(platform="ozon").order_by("-published_at").first()
|
||||
)
|
||||
latest_time = (
|
||||
latest_review.published_at.isoformat()
|
||||
if latest_review
|
||||
else "2000-01-01T00:00:00Z"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting latest review: {e}")
|
||||
latest_time = "2000-01-01T00:00:00Z"
|
||||
|
||||
reviews = await self.get_reviews()
|
||||
if not reviews:
|
||||
logger.info("No reviews fetched from Ozon")
|
||||
return {"processed": 0, "new": 0}
|
||||
|
||||
new_reviews = [r for r in reviews if r.get("published_at", "") > latest_time]
|
||||
if not new_reviews:
|
||||
logger.info("No new reviews found on Ozon")
|
||||
return {"processed": 0, "new": 0}
|
||||
|
||||
new_reviews.sort(key=lambda r: r.get("published_at", ""))
|
||||
|
||||
processed = 0
|
||||
for review in new_reviews:
|
||||
review_id = review.get("id")
|
||||
|
||||
existing = await Review.filter(
|
||||
external_id=review_id, platform="ozon"
|
||||
).exists()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
logger.info(f"Ozon: processing review {review}")
|
||||
|
||||
published_at = datetime.fromisoformat(
|
||||
review.get("published_at").replace("Z", "+00:00")
|
||||
).astimezone(config.TIMEZONE)
|
||||
|
||||
product_id = str(review.get("sku", ""))
|
||||
if not product_id:
|
||||
logger.error(f"Ozon: no product id for review {review}")
|
||||
continue
|
||||
|
||||
product = get_products_dict(get_cache_key()).get(product_id)
|
||||
if product:
|
||||
product.setdefault("Название", product.get("Товары"))
|
||||
product.setdefault("Категория", product.get("Тип товара"))
|
||||
product.setdefault("Подкатегория", product.get("Категория 2-го уровня"))
|
||||
else:
|
||||
logger.warning(f"Ozon: no product found for id {product_id}")
|
||||
product = {
|
||||
"Название": "неизвестно",
|
||||
"Категория": "неизвестно",
|
||||
"Подкатегория": "неизвестно",
|
||||
}
|
||||
|
||||
review_data = {
|
||||
"external_id": review_id,
|
||||
"platform": "ozon",
|
||||
"product_id": str(review.get("sku", "")),
|
||||
"rating": review.get("rating", 0),
|
||||
"is_good": review.get("rating", 0) > config.NEGATIVE_RATING,
|
||||
"text": review.get("text", ""),
|
||||
"product_name": product.get("Название", ""),
|
||||
"product_category": product.get("Категория", ""),
|
||||
"product_subcategory": product.get("Подкатегория", ""),
|
||||
"published_at": published_at,
|
||||
"processed": False,
|
||||
}
|
||||
|
||||
review_obj = await Review.create(**review_data)
|
||||
|
||||
settings = await Settings.get_from_context(review=review_obj)
|
||||
|
||||
if not review_obj.text and review_obj.is_good and not settings.auto_response_empty_enabled:
|
||||
add_to_message_queue(review_obj)
|
||||
await asyncio.sleep(0.1)
|
||||
processed += 1
|
||||
continue
|
||||
|
||||
if settings.analysis_enabled and review_obj.text:
|
||||
try:
|
||||
analysis = await gpt_client.analyze_review(settings, review_obj)
|
||||
|
||||
await review_obj.update_from_dict(
|
||||
gpt_client.extract_analysis_data(analysis)
|
||||
)
|
||||
await review_obj.save()
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing review {review_id}: {e}")
|
||||
|
||||
if settings.auto_response_enabled and review_obj.text:
|
||||
try:
|
||||
response = await gpt_client.generate_answer(settings, review_obj)
|
||||
if response:
|
||||
result = await self.post_comment(review_id, response)
|
||||
if "error" not in result:
|
||||
review_obj.response_text = response
|
||||
review_obj.response_id = result.get("comment_id")
|
||||
review_obj.responded_at = datetime.now(config.TIMEZONE)
|
||||
await review_obj.save()
|
||||
except Exception as e:
|
||||
logger.error(f"Error auto-responding to review {review_id}: {e}")
|
||||
|
||||
if settings.auto_response_empty_enabled and not review_obj.text:
|
||||
try:
|
||||
template = (
|
||||
(settings.template_empty_high_rating or config.TEMPLATE_EMPTY_HIGH_RATING)
|
||||
if review_obj.rating > config.NEGATIVE_RATING
|
||||
else (settings.template_empty_low_rating or config.TEMPLATE_EMPTY_LOW_RATING)
|
||||
)
|
||||
result = await self.post_comment(review_id, template)
|
||||
if "error" not in result:
|
||||
review_obj.response_text = template
|
||||
review_obj.response_id = result.get("comment_id")
|
||||
review_obj.responded_at = datetime.now(config.TIMEZONE)
|
||||
await review_obj.save()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error auto-responding (template) to review {review_id}: {e}"
|
||||
)
|
||||
|
||||
add_to_message_queue(review_obj)
|
||||
processed += 1
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return {"processed": processed, "new": len(new_reviews)}
|
||||
Reference in New Issue
Block a user