Правки по замечаниям

This commit is contained in:
firewookie
2026-03-09 13:34:25 +05:00
parent 34aae0dd26
commit 2dfd0e6452
14 changed files with 63 additions and 8514 deletions
-2
View File
@@ -665,8 +665,6 @@ RIOPAY_CURRENCY=RUB
RIOPAY_MIN_AMOUNT_KOPEKS=10000
RIOPAY_MAX_AMOUNT_KOPEKS=100000000
RIOPAY_WEBHOOK_PATH=/riopay-webhook
RIOPAY_WEBHOOK_HOST=0.0.0.0
RIOPAY_WEBHOOK_PORT=8090
# URL для редиректа после оплаты (опционально)
RIOPAY_SUCCESS_URL=
RIOPAY_FAIL_URL=
+1 -1
View File
@@ -1477,7 +1477,7 @@ CONTEST_BUTTON_VISIBLE=true
- 💳 **WATA**
- 💳 **Freekassa** (NSPK СБП + карты)
- 💳 **CloudPayments** (карты + СБП)
- - 💳 **RioPay** (карты + СБП)
- 💳 **RioPay** (карты + СБП)
- 🔥 Автогенерация счетов и webhook-уведомления
- 💼 История операций
- 🔄 Автоплатёж с настройкой дня списания
-2
View File
@@ -543,8 +543,6 @@ class Settings(BaseSettings):
RIOPAY_MIN_AMOUNT_KOPEKS: int = 10000 # 100₽
RIOPAY_MAX_AMOUNT_KOPEKS: int = 100000000 # 1 000 000₽
RIOPAY_WEBHOOK_PATH: str = '/riopay-webhook'
RIOPAY_WEBHOOK_HOST: str = '0.0.0.0'
RIOPAY_WEBHOOK_PORT: int = 8090
RIOPAY_SUCCESS_URL: str | None = None
RIOPAY_FAIL_URL: str | None = None
+48 -45
View File
@@ -4,13 +4,21 @@ from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from importlib import import_module
from typing import Any
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.database.crud.riopay import (
create_riopay_payment as crud_create_riopay_payment,
get_riopay_payment_by_order_id,
get_riopay_payment_by_riopay_order_id,
update_riopay_payment_status,
)
from app.database.crud.transaction import create_transaction
from app.database.crud.user import get_user_by_id
from app.database.models import PaymentMethod, TransactionType, User as UserModel
from app.services.riopay_service import riopay_service
from app.utils.payment_logger import payment_logger as logger
from app.utils.user_utils import format_referrer_info
@@ -68,8 +76,7 @@ class RioPayPaymentMixin:
return None
# Получаем telegram_id пользователя для order_id
payment_module = import_module('app.services.payment_service')
user = await payment_module.get_user_by_id(db, user_id)
user = await get_user_by_id(db, user_id)
tg_id = user.telegram_id if user else user_id
# Генерируем уникальный order_id с telegram_id для удобного поиска
@@ -111,11 +118,8 @@ class RioPayPaymentMixin:
'RioPay API: создан заказ', order_id=order_id, riopay_order_id=riopay_order_id, payment_url=payment_url
)
# Импортируем CRUD модуль
riopay_crud = import_module('app.database.crud.riopay')
# Сохраняем в БД
local_payment = await riopay_crud.create_riopay_payment(
local_payment = await crud_create_riopay_payment(
db=db,
user_id=user_id,
order_id=order_id,
@@ -157,31 +161,20 @@ class RioPayPaymentMixin:
db: AsyncSession,
*,
payload: dict[str, Any],
raw_body: bytes,
signature: str | None = None,
) -> bool:
"""
Обрабатывает webhook от RioPay.
Подпись проверяется в webserver/payments.py до вызова этого метода.
Args:
db: Сессия БД
payload: JSON тело webhook
raw_body: Сырое тело для проверки подписи
signature: Подпись из заголовка
Returns:
True если платеж успешно обработан
"""
try:
# Проверка подписи (обязательна)
if not signature:
logger.warning('RioPay webhook: отсутствует подпись')
return False
if not riopay_service.verify_webhook_signature(raw_body, signature):
logger.warning('RioPay webhook: неверная подпись')
return False
# Извлекаем данные из payload
riopay_order_id = payload.get('id')
external_id = payload.get('externalId')
@@ -192,15 +185,12 @@ class RioPayPaymentMixin:
logger.warning('RioPay webhook: отсутствуют обязательные поля', payload=payload)
return False
# Импортируем CRUD модуль
riopay_crud = import_module('app.database.crud.riopay')
# Ищем платеж по external_id (наш order_id) или riopay_order_id
payment = None
if external_id:
payment = await riopay_crud.get_riopay_payment_by_order_id(db, external_id)
payment = await get_riopay_payment_by_order_id(db, external_id)
if not payment and riopay_order_id:
payment = await riopay_crud.get_riopay_payment_by_riopay_order_id(db, riopay_order_id)
payment = await get_riopay_payment_by_riopay_order_id(db, riopay_order_id)
if not payment:
logger.warning(
@@ -227,10 +217,9 @@ class RioPayPaymentMixin:
'amount': amount,
'payment_type': payload.get('paymentType'),
'included_fee': payload.get('includedFee'),
'raw_payload': payload,
}
payment = await riopay_crud.update_riopay_payment_status(
payment = await update_riopay_payment_status(
db=db,
payment=payment,
status=internal_status,
@@ -240,6 +229,18 @@ class RioPayPaymentMixin:
callback_payload=callback_payload,
)
# Проверка суммы: payload.amount должен совпадать с суммой в базе
if is_paid and amount is not None:
expected = payment.amount_kopeks / 100
if abs(float(amount) - expected) > 0.01:
logger.error(
'RioPay amount mismatch',
expected=expected,
received=amount,
order_id=payment.order_id,
)
return False
# Финализируем платеж если оплачен
if is_paid:
return await self._finalize_riopay_payment(
@@ -261,14 +262,12 @@ class RioPayPaymentMixin:
trigger: str,
) -> bool:
"""Создаёт транзакцию, начисляет баланс и отправляет уведомления."""
payment_module = import_module('app.services.payment_service')
if payment.transaction_id:
logger.info('RioPay платеж уже привязан к транзакции', order_id=payment.order_id, trigger=trigger)
return True
# Получаем пользователя
user = await payment_module.get_user_by_id(db, payment.user_id)
user = await get_user_by_id(db, payment.user_id)
if not user:
logger.error(
'Пользователь не найден для RioPay платежа',
@@ -279,7 +278,7 @@ class RioPayPaymentMixin:
return False
# Создаем транзакцию
transaction = await payment_module.create_transaction(
transaction = await create_transaction(
db,
user_id=payment.user_id,
type=TransactionType.DEPOSIT,
@@ -292,8 +291,7 @@ class RioPayPaymentMixin:
)
# Связываем платеж с транзакцией
riopay_crud = import_module('app.database.crud.riopay')
await riopay_crud.update_riopay_payment_status(
await update_riopay_payment_status(
db=db,
payment=payment,
status=payment.status,
@@ -303,13 +301,20 @@ class RioPayPaymentMixin:
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
# Начисляем баланс
user.balance_kopeks += payment.amount_kopeks
user.updated_at = datetime.now(UTC)
# Обновляем флаг первого пополнения в том же коммите
# Атомарное начисление баланса через SQL UPDATE, чтобы избежать race condition
# при одновременных вебхуках / check_riopay_payment_status
update_values: dict[str, Any] = {
UserModel.balance_kopeks: UserModel.balance_kopeks + payment.amount_kopeks,
UserModel.updated_at: datetime.now(UTC),
}
if was_first_topup:
user.has_made_first_topup = True
update_values[UserModel.has_made_first_topup] = True
await db.execute(
update(UserModel)
.where(UserModel.id == user.id)
.values(update_values)
)
promo_group = user.get_primary_promo_group()
subscription = getattr(user, 'subscription', None)
@@ -401,9 +406,7 @@ class RioPayPaymentMixin:
Проверяет статус платежа через API.
"""
try:
riopay_crud = import_module('app.database.crud.riopay')
payment = await riopay_crud.get_riopay_payment_by_order_id(db, order_id)
payment = await get_riopay_payment_by_order_id(db, order_id)
if not payment:
logger.warning('RioPay payment not found', order_id=order_id)
return None
@@ -433,7 +436,7 @@ class RioPayPaymentMixin:
'riopay_order_data': order_data,
}
payment = await riopay_crud.update_riopay_payment_status(
payment = await update_riopay_payment_status(
db=db,
payment=payment,
status='success',
@@ -451,7 +454,7 @@ class RioPayPaymentMixin:
)
elif internal_status != payment.status:
# Обновляем статус если изменился
payment = await riopay_crud.update_riopay_payment_status(
payment = await update_riopay_payment_status(
db=db,
payment=payment,
status=internal_status,
+2 -1
View File
@@ -119,7 +119,8 @@ class RioPayService:
except Exception:
error_msg = text
logger.error('RioPay create_order error', status_code=response.status, error_msg=error_msg)
logger.error('RioPay create_order error', status_code=response.status)
logger.debug('RioPay create_order error details', error_msg=error_msg)
raise RioPayAPIError(response.status, error_msg)
except aiohttp.ClientError as e:
-2
View File
@@ -1152,8 +1152,6 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
success = await payment_service.process_riopay_webhook(
db,
payload=payload,
raw_body=raw_body,
signature=signature,
)
if success:
return JSONResponse({'status': 'ok'}, status_code=status.HTTP_200_OK)
+6 -5
View File
@@ -95,13 +95,14 @@ volumes:
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
# ipam:
# config:
# - subnet: 172.20.0.0/16
# gateway: 172.20.0.1
driver_opts:
com.docker.network.driver.mtu: 1350
remnawave-network:
name: remnawave-network
external: true # Используем существующую сеть панели
driver: bridge
# external: true # Используем существующую сеть панели
View File
-1729
View File
File diff suppressed because it is too large Load Diff
-1747
View File
File diff suppressed because it is too large Load Diff
-1750
View File
File diff suppressed because it is too large Load Diff
-1616
View File
File diff suppressed because it is too large Load Diff
-1614
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -38,6 +38,7 @@ from app.services.payment_verification_service import (
from app.services.referral_contest_service import referral_contest_service
from app.services.remnawave_sync_service import remnawave_sync_service
from app.services.reporting_service import reporting_service
from app.services.riopay_service import riopay_service
from app.services.system_settings_service import bot_configuration_service
from app.services.traffic_monitoring_service import traffic_monitoring_scheduler
from app.services.version_service import version_service
@@ -934,6 +935,11 @@ async def main():
except Exception as error:
logger.error('Ошибка остановки веб-API', error=error)
try:
await riopay_service.close()
except Exception as e:
logger.error('Ошибка закрытия сессии RioPay', error=e)
if 'bot' in locals():
try:
await bot.session.close()