feat: initialize full-stack architecture with backend routes, database schema, and glassmorphic frontend UI

This commit is contained in:
2026-05-13 00:12:19 +03:00
parent bd73f18d5c
commit 0732acbda5
21 changed files with 1937 additions and 307 deletions

28
backend/Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# backend/Dockerfile
FROM python:3.9-slim
WORKDIR /app
# Установка системных зависимостей для psycopg2 и др.
RUN apt-get update && apt-get install -y \
libpq-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Копирование и установка зависимостей
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование кода бэкенда
COPY backend/ ./backend/
# Создание __init__.py если их нет (для корректной работы относительных импортов)
RUN touch backend/__init__.py backend/routes/__init__.py
# Переменная окружения для корректных импортов
ENV PYTHONPATH=/app
EXPOSE 5000
# Запуск приложения
CMD ["python", "backend/app.py"]

View File

@@ -2,30 +2,75 @@ import os
from flask import Flask
from flask_cors import CORS
from dotenv import load_dotenv
from flasgger import Swagger
from flask_socketio import SocketIO, emit
# Load environment variables from .env
load_dotenv()
app = Flask(__name__)
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")
# Swagger setup
# ... (keeping existing swagger config)
app.config['SWAGGER'] = {
'title': 'AutoMarket API',
'uiversion': 3,
'specs_route': '/api/docs/',
'securityDefinitions': {
'APIKeyHeader': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header',
'description': 'Введите токен в формате: Bearer <ваш_токен>'
}
}
}
swagger = Swagger(app)
# Ensure upload folder exists
# ...
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# Register blueprints (routes)
# Register blueprints
from .routes.cases import cases_blueprint
from .routes.credentials import credentials_blueprint
from .routes.auth import auth_blueprint
from .routes.dashboard import dashboard_blueprint
from .routes.reports import reports_blueprint
from .routes.ai import ai_blueprint
from .routes.seo import seo_blueprint
from .routes.content import content_blueprint
from .routes.finance import finance_blueprint
app.register_blueprint(cases_blueprint, url_prefix='/api')
app.register_blueprint(credentials_blueprint, url_prefix='/api')
app.register_blueprint(auth_blueprint, url_prefix='/api')
app.register_blueprint(dashboard_blueprint, url_prefix='/api')
app.register_blueprint(reports_blueprint, url_prefix='/api')
app.register_blueprint(ai_blueprint, url_prefix='/api')
app.register_blueprint(seo_blueprint, url_prefix='/api')
app.register_blueprint(content_blueprint, url_prefix='/api')
app.register_blueprint(finance_blueprint, url_prefix='/api')
import threading
import time
def background_scanner():
"""Фоновая задача для имитации сканирования маркетплейсов."""
while True:
# Здесь в будущем будет вызов api.ozon_api и т.д.
print("--- [Scanner] Checking marketplaces for price dumping... ---")
# Эмитируем событие для примера (что сканер что-то нашел)
# socketio.emit('scanner_update', {'message': 'Сканер проверил 10 товаров...'}, namespace='/')
time.sleep(60)
# Start background thread
scanner_thread = threading.Thread(target=background_scanner, daemon=True)
scanner_thread.start()
if __name__ == '__main__':
# Use host 0.0.0.0 for Docker compatibility, port 5000
app.run(host='0.0.0.0', port=5000, debug=True)
# Use socketio.run instead of app.run
socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)

View File

@@ -8,3 +8,8 @@ PyJWT==2.8.0
bcrypt==4.1.3
reportlab==4.1.0
pandas==2.2.1
flasgger==0.9.7.1
flask-socketio==5.3.6
google-generativeai==0.5.2
ImageHash==4.3.1
Pillow==10.2.0

79
backend/routes/ai.py Normal file
View File

@@ -0,0 +1,79 @@
from flask import Blueprint, request, jsonify
from ..middleware import token_required
from ..utils.ai_utils import generate_complaint_text
ai_blueprint = Blueprint('ai', __name__)
@ai_blueprint.route('/ai/generate-complaint', methods=['POST'])
@token_required
def ai_generate_complaint(current_user_id):
"""
Генерация текста претензии с помощью ИИ
---
tags:
- AI
security:
- APIKeyHeader: []
parameters:
- name: body
in: body
required: true
schema:
type: object
properties:
platform:
type: string
violator_article:
type: string
my_article:
type: string
responses:
200:
description: Сгенерированный текст
"""
data = request.json
platform = data.get('platform', 'ozon')
v_art = data.get('violator_article', '')
my_art = data.get('my_article', '')
if not v_art:
return jsonify({"error": "Артикул нарушителя обязателен"}), 400
text = generate_complaint_text(platform, v_art, my_art)
return jsonify({"text": text})
@ai_blueprint.route('/ai/generate-reply', methods=['POST'])
@token_required
def ai_generate_reply(current_user_id):
"""
Генерация ответа на отзыв покупателя
---
tags:
- AI
security:
- APIKeyHeader: []
parameters:
- name: body
in: body
required: true
schema:
type: object
properties:
review_text:
type: string
rating:
type: integer
responses:
200:
description: Сгенерированный ответ
"""
data = request.json
review_text = data.get('review_text', '')
rating = data.get('rating', 5)
if not review_text:
return jsonify({"error": "Текст отзыва обязателен"}), 400
from ..utils.ai_utils import generate_review_reply
text = generate_review_reply(review_text, rating)
return jsonify({"text": text})

View File

@@ -11,6 +11,26 @@ auth_blueprint = Blueprint('auth', __name__)
@auth_blueprint.route('/auth/register', methods=['POST'])
def register():
"""
Регистрация нового пользователя
---
tags:
- Auth
parameters:
- name: body
in: body
required: true
schema:
type: object
properties:
username:
type: string
password:
type: string
responses:
201:
description: Пользователь успешно зарегистрирован
"""
data = request.json
username = data.get('username')
password = data.get('password')
@@ -41,6 +61,30 @@ def register():
@auth_blueprint.route('/auth/login', methods=['POST'])
def login():
"""
Вход в систему
---
tags:
- Auth
parameters:
- name: body
in: body
required: true
schema:
type: object
properties:
username:
type: string
password:
type: string
responses:
200:
description: Токен успешно сгенерирован
schema:
properties:
token:
type: string
"""
data = request.json
username = data.get('username')
password = data.get('password')
@@ -59,6 +103,11 @@ def login():
'exp': datetime.utcnow() + timedelta(days=7)
}, SECRET_KEY, algorithm="HS256")
# --- AUDIT LOG ---
from ..utils.audit_utils import log_action
log_action(user[0], 'LOGIN', {'ip': request.remote_addr})
# -----------------
return jsonify({"token": token})
@auth_blueprint.route('/auth/me', methods=['GET'])

View File

@@ -27,6 +27,30 @@ def serialize_case(row):
@cases_blueprint.route('/cases', methods=['GET'])
@token_required
def list_cases(current_user_id):
"""
Получить список всех кейсов пользователя
---
tags:
- Cases
security:
- APIKeyHeader: []
parameters:
- name: article
in: query
type: string
description: Фильтр по артикулу
- name: status
in: query
type: string
description: Фильтр по статусу
- name: platform
in: query
type: string
description: Фильтр по площадке
responses:
200:
description: Список кейсов
"""
# optional filters: ?article=...&status=...&platform=...
article = request.args.get('article')
status = request.args.get('status')
@@ -56,13 +80,55 @@ def list_cases(current_user_id):
@cases_blueprint.route('/cases', methods=['POST'])
@token_required
def create_case(current_user_id):
"""
Создать новый кейс
---
tags:
- Cases
security:
- APIKeyHeader: []
parameters:
- name: body
in: body
required: true
schema:
type: object
required: [platform, violator_url, violator_article]
properties:
platform:
type: string
violator_url:
type: string
violator_article:
type: string
my_article:
type: string
comment:
type: string
responses:
201:
description: Кейс создан
"""
data = request.json
required = ["platform", "violator_url", "violator_article"]
for f in required:
if f not in data:
return jsonify({"error": f"Missing field {f}"}), 400
# --- PLAN LIMIT CHECK ---
conn = get_db_connection()
cur = conn.cursor()
cur.execute("SELECT plan FROM users WHERE id = %s", (current_user_id,))
user_plan = cur.fetchone()[0]
if user_plan == 'free':
cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s", (current_user_id,))
count = cur.fetchone()[0]
if count >= 5:
release_db_connection(conn)
return jsonify({"error": "Лимит кейсов для Free тарифа (5 шт) исчерпан. Перейдите на PRO."}), 403
# ------------------------
cur.execute(
"""INSERT INTO cases (user_id, platform, violator_url, violator_article, my_article, comment, status)
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id""",
@@ -79,6 +145,40 @@ def create_case(current_user_id):
case_id = cur.fetchone()[0]
conn.commit()
release_db_connection(conn)
# --- TELEGRAM NOTIFICATION ---
from ..utils.telegram_utils import send_telegram_notification
msg = (
f"🚀 <b>Новый кейс #{case_id}</b>\n"
f"Площадка: {data.get('platform').upper()}\n"
f"Артикул: {data.get('violator_article')}\n"
f"Статус: NEW"
)
send_telegram_notification(msg)
# -----------------------------
# --- AUDIT LOG ---
from ..utils.audit_utils import log_action
log_action(current_user_id, 'CREATE_CASE', {
'case_id': case_id,
'platform': data.get('platform'),
'article': data.get('violator_article')
})
# -----------------
# --- WEBSOCKET NOTIFICATION ---
try:
from ..app import socketio
socketio.emit('case_created', {
'id': case_id,
'platform': data.get('platform'),
'article': data.get('violator_article'),
'message': f"Новый кейс на {data.get('platform').upper()} создан!"
})
except Exception as e:
print(f"WS Emit Error: {e}")
# ------------------------------
return jsonify({"id": case_id}), 201
@cases_blueprint.route('/cases/<int:case_id>', methods=['GET'])
@@ -123,6 +223,12 @@ def delete_case(current_user_id, case_id):
cur.execute("DELETE FROM cases WHERE id = %s AND user_id = %s", (case_id, current_user_id))
conn.commit()
release_db_connection(conn)
# --- AUDIT LOG ---
from ..utils.audit_utils import log_action
log_action(current_user_id, 'DELETE_CASE', {'case_id': case_id})
# -----------------
return jsonify({"message": "Case deleted"})
# ---------- FILE UPLOAD ----------

76
backend/routes/content.py Normal file
View File

@@ -0,0 +1,76 @@
import os
from flask import Blueprint, request, jsonify, current_app
from werkzeug.utils import secure_filename
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
from ..utils.image_utils import calculate_image_hash
content_blueprint = Blueprint('content', __name__)
@content_blueprint.route('/content/reference', methods=['POST'])
@token_required
def upload_reference(current_user_id):
if 'file' not in request.files:
return jsonify({"error": "No file"}), 400
file = request.files['file']
article = request.form.get('article', 'unknown')
filename = secure_filename(file.filename)
save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], f"ref_{current_user_id}_{filename}")
file.save(save_path)
# Calculate Hash
img_hash = calculate_image_hash(save_path)
conn = get_db_connection()
cur = conn.cursor()
cur.execute(
"INSERT INTO reference_images (user_id, file_path, image_hash, article) VALUES (%s, %s, %s, %s) RETURNING id",
(current_user_id, save_path, img_hash, article)
)
ref_id = cur.fetchone()[0]
conn.commit()
release_db_connection(conn)
return jsonify({"id": ref_id, "hash": img_hash}), 201
@content_blueprint.route('/content/reference', methods=['GET'])
@token_required
def list_references(current_user_id):
conn = get_db_connection()
cur = conn.cursor()
cur.execute("SELECT id, file_path, image_hash, article, created_at FROM reference_images WHERE user_id = %s", (current_user_id,))
rows = cur.fetchall()
release_db_connection(conn)
return jsonify([{
"id": r[0],
"filename": os.path.basename(r[1]),
"hash": r[2],
"article": r[3],
"date": r[4].isoformat()
} for r in rows])
@content_blueprint.route('/content/detect-clones/<int:ref_id>', methods=['GET'])
@token_required
def detect_clones(current_user_id, ref_id):
# В реальном приложении здесь запускается поиск по маркетплейсам
# Для демо имитируем нахождение нескольких нарушителей
clones = [
{
"platform": "ozon",
"url": "https://ozon.ru/product/123456",
"article": "OZ-9988",
"similarity": "98%",
"image_url": "https://ir.ozone.ru/s3/multimedia-1/654321.jpg"
},
{
"platform": "wildberries",
"url": "https://wildberries.ru/catalog/778899/detail.aspx",
"article": "WB-CLONE-1",
"similarity": "92%",
"image_url": "https://basket-01.wb.ru/vol77/part778/778899/images/big/1.jpg"
}
]
return jsonify(clones)

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, jsonify
from datetime import datetime, timedelta
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
@@ -7,6 +8,17 @@ dashboard_blueprint = Blueprint('dashboard', __name__)
@dashboard_blueprint.route('/dashboard/stats', methods=['GET'])
@token_required
def get_stats(current_user_id):
"""
Получить общую статистику для дашборда
---
tags:
- Dashboard
security:
- APIKeyHeader: []
responses:
200:
description: Объект со статистикой и трендами
"""
conn = get_db_connection()
cur = conn.cursor()
@@ -39,13 +51,76 @@ def get_stats(current_user_id):
"date": r[4].isoformat() if r[4] else None
} for r in recent_rows
]
# 6. Trend data (last 7 days)
trend = []
for i in range(6, -1, -1):
day = (datetime.utcnow() - timedelta(days=i)).date()
cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s AND created_at::date = %s", (current_user_id, day))
count = cur.fetchone()[0]
trend.append({
"date": day.strftime('%d.%m'),
"count": count
})
# 5. Multi-account stores summary
cur.execute("SELECT id, platform, name FROM credentials WHERE user_id = %s", (current_user_id,))
stores_rows = cur.fetchall()
stores = []
total_sales = 0
for s in stores_rows:
# В реальной жизни здесь был бы вызов API Ozon/WB для получения продаж за сегодня
# Для демонстрации генерируем случайные данные
import random
daily_sales = random.randint(5000, 50000)
total_sales += daily_sales
stores.append({
"id": s[0],
"platform": s[1],
"name": s[2],
"status": "active",
"daily_sales": daily_sales
})
release_db_connection(conn)
return jsonify({
"total_cases": total_cases,
"cases_by_status": cases_by_status,
"total_integrations": total_integrations,
"total_sales": total_sales,
"platforms": platforms,
"recent_activity": recent_activity
"recent_activity": recent_activity,
"trend": trend,
"stores": stores
})
@dashboard_blueprint.route('/dashboard/logs', methods=['GET'])
@token_required
def get_logs(current_user_id):
"""
Получить лог аудита действий пользователя
---
tags:
- Dashboard
security:
- APIKeyHeader: []
responses:
200:
description: Список действий из лога
"""
conn = get_db_connection()
cur = conn.cursor()
cur.execute("SELECT action, details, created_at FROM audit_log WHERE user_id = %s ORDER BY created_at DESC LIMIT 20", (current_user_id,))
rows = cur.fetchall()
release_db_connection(conn)
logs = [
{
"action": r[0],
"details": r[1],
"date": r[2].isoformat() if r[2] else None
} for r in rows
]
return jsonify(logs)

47
backend/routes/finance.py Normal file
View File

@@ -0,0 +1,47 @@
from flask import Blueprint, request, jsonify
from ..middleware import token_required
finance_blueprint = Blueprint('finance', __name__)
@finance_blueprint.route('/finance/calculate-pnl', methods=['POST'])
@token_required
def calculate_pnl(current_user_id):
"""
Расчет чистой прибыли и маржинальности.
"""
data = request.json
# Входные данные
sale_price = float(data.get('sale_price', 0)) # Цена продажи
purchase_price = float(data.get('purchase_price', 0)) # Себестоимость (закупка)
logistics = float(data.get('logistics', 0)) # Логистика
commission_pct = float(data.get('commission_pct', 0)) # Комиссия маркетплейса %
tax_pct = float(data.get('tax_pct', 6)) # Налог % (УСН)
adv_per_unit = float(data.get('adv_per_unit', 0)) # Реклама на 1 шт
other_costs = float(data.get('other_costs', 0)) # Прочие расходы (упаковка и т.д.)
# Расчеты
marketplace_commission = sale_price * (commission_pct / 100)
taxes = sale_price * (tax_pct / 100)
total_costs = (
purchase_price +
logistics +
marketplace_commission +
taxes +
adv_per_unit +
other_costs
)
net_profit = sale_price - total_costs
margin = (net_profit / sale_price * 100) if sale_price > 0 else 0
roi = (net_profit / purchase_price * 100) if purchase_price > 0 else 0
return jsonify({
"net_profit": round(net_profit, 2),
"margin": round(margin, 2),
"roi": round(roi, 2),
"total_costs": round(total_costs, 2),
"tax_amount": round(taxes, 2),
"commission_amount": round(marketplace_commission, 2)
})

View File

@@ -1,20 +1,55 @@
import io
import os
import pandas as pd
from flask import Blueprint, send_file, jsonify, make_response
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
reports_blueprint = Blueprint('reports', __name__)
# Попытка зарегистрировать кириллический шрифт
FONT_PATH = os.path.join(os.path.dirname(__file__), '..', 'fonts', 'DejaVuSans.ttf')
FONT_NAME = 'DejaVuSans'
try:
if os.path.exists(FONT_PATH):
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
else:
# Пытаемся найти системный шрифт на Windows или Linux
sys_fonts = [
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'C:\\Windows\\Fonts\\arial.ttf'
]
for f in sys_fonts:
if os.path.exists(f):
pdfmetrics.registerFont(TTFont(FONT_NAME, f))
break
except Exception as e:
print(f"Warning: Could not register Cyrillic font: {e}")
FONT_NAME = 'Helvetica' # Fallback
@reports_blueprint.route('/reports/cases/csv', methods=['GET'])
@token_required
def export_cases_csv(current_user_id):
"""
Экспорт всех кейсов в CSV
---
tags:
- Reports
security:
- APIKeyHeader: []
responses:
200:
description: CSV файл
"""
conn = get_db_connection()
query = """
SELECT id, platform, violator_url, violator_article, my_article, comment, status, created_at
@@ -38,6 +73,22 @@ def export_cases_csv(current_user_id):
@reports_blueprint.route('/reports/case/<int:case_id>/pdf', methods=['GET'])
@token_required
def export_case_pdf(current_user_id, case_id):
"""
Генерация PDF претензии для конкретного кейса
---
tags:
- Reports
security:
- APIKeyHeader: []
parameters:
- name: case_id
in: path
type: integer
required: true
responses:
200:
description: PDF файл претензии
"""
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
@@ -56,34 +107,62 @@ def export_case_pdf(current_user_id, case_id):
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=letter)
styles = getSampleStyleSheet()
# Custom style for Russian text
ru_style = ParagraphStyle(
'Russian',
parent=styles['Normal'],
fontName=FONT_NAME,
fontSize=10,
leading=12
)
title_style = ParagraphStyle(
'RussianTitle',
parent=styles['Title'],
fontName=FONT_NAME,
fontSize=18,
spaceAfter=20
)
elements = []
# Title
elements.append(Paragraph(f"Официальная претензия #{case_id}", styles['Title']))
elements.append(Spacer(1, 12))
# Header / Title
elements.append(Paragraph(f"Официальная претензия #{case_id}", title_style))
elements.append(Paragraph(f"Система мониторинга AutoMarket (OzonGuard)", ru_style))
elements.append(Spacer(1, 20))
# Info table
data = [
["Маркетплейс", platform.upper()],
["Дата создания", created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"],
["Артикул нарушителя", v_art],
["Ссылка на товар", Paragraph(v_url, styles['Normal'])],
["Ваш артикул", my_art or "-"],
["Текущий статус", status.upper()]
[Paragraph("Параметр", ru_style), Paragraph("Значение", ru_style)],
[Paragraph("Маркетплейс", ru_style), platform.upper()],
[Paragraph("Дата создания", ru_style), created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"],
[Paragraph("Артикул нарушителя", ru_style), v_art],
[Paragraph("Ссылка на товар", ru_style), Paragraph(v_url, ru_style)],
[Paragraph("Ваш артикул", ru_style), my_art or "-"],
[Paragraph("Текущий статус", ru_style), status.upper()]
]
t = Table(data, colWidths=[150, 350])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
('GRID', (0, 0), (-1, -1), 1, colors.black),
('PADDING', (0, 0), (-1, -1), 6),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), FONT_NAME),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
('GRID', (0, 0), (-1, -1), 1, colors.grey),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
]))
elements.append(t)
elements.append(Spacer(1, 24))
# Description
elements.append(Paragraph("Описание нарушения:", styles['Heading2']))
elements.append(Paragraph(comment or "Комментарий отсутствует", styles['Normal']))
elements.append(Paragraph("Описание нарушения:", ParagraphStyle('Sub', parent=ru_style, fontSize=12, spaceAfter=10)))
elements.append(Paragraph(comment or "Комментарий отсутствует", ru_style))
elements.append(Spacer(1, 40))
elements.append(Paragraph("Сгенерировано автоматически. Данный документ является официальным уведомлением о нарушении правил платформы.", ParagraphStyle('Footer', parent=ru_style, fontSize=8, textColor=colors.grey)))
doc.build(elements)
buffer.seek(0)

78
backend/routes/seo.py Normal file
View File

@@ -0,0 +1,78 @@
from flask import Blueprint, request, jsonify
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
import random
from datetime import datetime, timedelta
seo_blueprint = Blueprint('seo', __name__)
@seo_blueprint.route('/seo/keywords', methods=['GET'])
@token_required
def list_keywords(current_user_id):
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
SELECT k.id, k.keyword, k.article, k.platform,
(SELECT position FROM seo_history h WHERE h.keyword_id = k.id ORDER BY checked_at DESC LIMIT 1) as last_pos
FROM seo_keywords k
WHERE k.user_id = %s
""", (current_user_id,))
rows = cur.fetchall()
release_db_connection(conn)
return jsonify([{
"id": r[0],
"keyword": r[1],
"article": r[2],
"platform": r[3],
"last_position": r[4]
} for r in rows])
@seo_blueprint.route('/seo/keywords', methods=['POST'])
@token_required
def add_keyword(current_user_id):
data = request.json
keyword = data.get('keyword')
article = data.get('article')
platform = data.get('platform', 'ozon')
if not keyword or not article:
return jsonify({"error": "Keyword and article are required"}), 400
conn = get_db_connection()
cur = conn.cursor()
cur.execute(
"INSERT INTO seo_keywords (user_id, keyword, article, platform) VALUES (%s, %s, %s, %s) RETURNING id",
(current_user_id, keyword, article, platform)
)
kid = cur.fetchone()[0]
# Generate some mock history immediately
for i in range(7):
date = datetime.now() - timedelta(days=i)
pos = random.randint(1, 150)
cur.execute("INSERT INTO seo_history (keyword_id, position, checked_at) VALUES (%s, %s, %s)", (kid, pos, date))
conn.commit()
release_db_connection(conn)
return jsonify({"id": kid}), 201
@seo_blueprint.route('/seo/history/<int:keyword_id>', methods=['GET'])
@token_required
def get_seo_history(current_user_id, keyword_id):
conn = get_db_connection()
cur = conn.cursor()
# Check ownership
cur.execute("SELECT id FROM seo_keywords WHERE id = %s AND user_id = %s", (keyword_id, current_user_id))
if not cur.fetchone():
release_db_connection(conn)
return jsonify({"error": "Not found"}), 404
cur.execute("SELECT position, checked_at FROM seo_history WHERE keyword_id = %s ORDER BY checked_at ASC", (keyword_id,))
rows = cur.fetchall()
release_db_connection(conn)
return jsonify([{
"position": r[0],
"date": r[1].strftime('%d.%m')
} for r in rows])

65
backend/utils/ai_utils.py Normal file
View File

@@ -0,0 +1,65 @@
import os
import google.generativeai as genai
def generate_complaint_text(platform, violator_article, my_article=None):
"""Генерирует текст претензии с помощью Google Gemini."""
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
return "Ошибка: API ключ для Gemini не настроен в .env"
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-1.5-flash')
prompt = f"""
Ты - профессиональный юрист в сфере интеллектуальной собственности и e-commerce.
Напиши официальную и убедительную претензию для маркетплейса {platform}.
Суть нарушения: продавец с артикулом {violator_article} незаконно использует мой контент или нарушает правила ценообразования.
Мой оригинальный артикул для сравнения: {my_article if my_article else 'не указан'}.
Требования:
1. Текст должен быть на русском языке.
2. Стиль официально-деловой.
3. Упомяни о необходимости защиты прав правообладателя.
4. Текст должен быть лаконичным (до 500 знаков).
Ответ дай сразу в виде текста претензии, без лишних вступлений.
"""
try:
response = model.generate_content(prompt)
return response.text.strip()
except Exception as e:
return f"Ошибка при генерации текста ИИ: {str(e)}"
def generate_review_reply(review_text, rating):
"""Генерирует ответ на отзыв покупателя."""
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
return "Ошибка: API ключ не настроен."
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-1.5-flash')
prompt = f"""
Ты - менеджер по работе с клиентами в магазине на маркетплейсе.
Твоя задача: вежливо и профессионально ответить на отзыв покупателя.
Оценка покупателя: {rating} звезд(ы).
Текст отзыва: "{review_text}"
Правила:
1. Если оценка 4-5 звезд: поблагодари за покупку, вырази радость, предложи посмотреть другие товары нашего бренда.
2. Если оценка 1-3 звезды: извинись за неудобства, уточни, что мы работаем над качеством, и предложи обратиться в поддержку, если это уместно.
3. Стиль: дружелюбный, но деловой. Не используй слишком много восклицательных знаков.
4. Длина: 2-4 предложения.
5. Не используй шаблоны "Уважаемый покупатель", начни более естественно.
Ответ дай сразу в виде готового текста.
"""
try:
response = model.generate_content(prompt)
return response.text.strip()
except Exception as e:
return f"Ошибка ИИ: {str(e)}"

View File

@@ -0,0 +1,17 @@
import json
from ..db import get_db_connection, release_db_connection
def log_action(user_id, action, details=None):
"""Записывает действие пользователя в лог аудита."""
conn = get_db_connection()
try:
cur = conn.cursor()
cur.execute(
"INSERT INTO audit_log (user_id, action, details) VALUES (%s, %s, %s)",
(user_id, action, json.dumps(details) if details else None)
)
conn.commit()
except Exception as e:
print(f"Audit Log Error: {e}")
finally:
release_db_connection(conn)

View File

@@ -0,0 +1,20 @@
from PIL import Image
import imagehash
import os
def calculate_image_hash(image_path):
"""Вычисляет перцептивный хеш изображения."""
try:
img = Image.open(image_path)
hash_val = imagehash.phash(img)
return str(hash_val)
except Exception as e:
print(f"Hashing Error: {e}")
return None
def compare_hashes(hash1, hash2, threshold=5):
"""Сравнивает два хеша. Если разница меньше порога, изображения считаются похожими."""
h1 = imagehash.hex_to_hash(hash1)
h2 = imagehash.hex_to_hash(hash2)
diff = h1 - h2
return diff <= threshold

View File

@@ -0,0 +1,24 @@
import requests
import os
def send_telegram_notification(message):
token = os.getenv('TELEGRAM_BOT_TOKEN')
chat_id = os.getenv('TELEGRAM_CHAT_ID')
if not token or not chat_id:
print("Telegram notification skipped: Token or Chat ID not configured.")
return False
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {
"chat_id": chat_id,
"text": message,
"parse_mode": "HTML"
}
try:
response = requests.post(url, json=payload, timeout=5)
return response.status_code == 200
except Exception as e:
print(f"Error sending Telegram message: {e}")
return False