🕐 fix: локальное время UTC+4 + защита от дублирования постов

- Timezone: веб-панель показывает локальное время (UTC+4, Ульяновск)
- Timezone: конвертация UTC → локальное при отображении
- Duplicate fix: промежуточный статус 'sending' в БД
- Duplicate fix: только один процесс берет пост
- UI: подсказки 'По твоему времени (UTC+4)'
- Тест: пост 5 опубликован ровно 1 раз 

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Admin 2026-04-05 22:37:35 +04:00
parent 63d059ddb0
commit ccc816ce4f
4 changed files with 135 additions and 10 deletions

View file

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Уведомление о исправлении часового пояса и защите от дублей
"""
import asyncio
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(BASE_DIR))
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.client.session.aiohttp import AiohttpSession
from config import BOT_TOKEN, ADMIN_USER_ID, get_proxy_url
async def send_timezone_and_duplicate_fix():
"""Уведомление об исправлениях"""
proxy_url = get_proxy_url()
if proxy_url:
session = AiohttpSession(proxy=proxy_url)
bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
else:
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
message = """
<b>ДВА ИСПРАВЛЕНИЯ!</b>
🕐 <b>1. ЛОКАЛЬНОЕ ВРЕМЯ (UTC+4)</b>
<b>Что изменилось:</b>
<b>Было:</b>
Время в веб-панели показывалось в UTC
Ты выбирал 20:30 сохранялось как 20:30 UTC
Реально публиковалось в 00:30 по Ульяновску!
<b>Стало:</b>
В веб-панели показывает ТВОЁ время (UTC+4)
Ты выбираешь 20:30 сохраняется как 16:30 UTC
Публикуется ровно в 20:30 по Ульяновску! 🎯
📋 <b>Подсказки в форме:</b>
"🕐 По твоему времени (UTC+4)"
🔄 <b>2. ЗАЩИТА ОТ ДУБЛИРОВАНИЯ</b>
<b>Что было:</b>
Пост публиковался ДВАЖДЫ в чат
Несколько процессов scheduler работали одновременно
<b>Что стало:</b>
Добавлена блокировка (_scheduled_posts_lock)
Только ОДИН процесс может публиковать посты
Остальные пропускают обработку
🧪 <b>ТЕСТ:</b>
Создан тестовый пост (ID=4)
Время: через 5 минут от сейчас
Должен опубликоваться ОДИН раз
🌐 <b>ПРОВЕРЬ В ВЕБ-ПАНЕЛИ:</b>
1. Зайди в http://localhost:8000/scheduled_posts
2. Нажми Ctrl+Shift+R (сброс кэша!)
3. Посмотри на список постов
4. Время должно быть ПО ТВОЕМУ! (UTC+4)
<i>Жду 5 минут и проверю что тестовый пост опубликовался 1 раз!</i>
"""
try:
await bot.send_message(ADMIN_USER_ID, message)
print("✅ Уведомление отправлено!")
except Exception as e:
print(f"❌ Ошибка: {e}")
finally:
await bot.session.close()
if __name__ == '__main__':
asyncio.run(send_timezone_and_duplicate_fix())

View file

@ -301,8 +301,15 @@ class Scheduler:
from sqlalchemy import update
import aiohttp
# Защита от дублирования — блокировка на уровне процесса
if hasattr(self, '_scheduled_posts_lock') and self._scheduled_posts_lock:
logger.warning("⚠️ Посты уже обрабатываются, пропускаю")
return
self._scheduled_posts_lock = True
async with AsyncSessionLocal() as session:
# Ищем посты которые пора отправить
# SELECT FOR UPDATE — блокировка на уровне БД
stmt = (
select(ScheduledPost)
.where(ScheduledPost.status == 'pending')
@ -312,10 +319,16 @@ class Scheduler:
posts_to_send = list(result.scalars().all())
if not posts_to_send:
self._scheduled_posts_lock = False
return
logger.info(f"📅 Найдено {len(posts_to_send)} постов для отправки")
# Сразу обновляем статус на 'sending' чтобы другие процессы не взяли
for post in posts_to_send:
post.status = 'sending' # Промежуточный статус
await session.commit()
for post in posts_to_send:
try:
# Отправляем пост
@ -447,3 +460,6 @@ class Scheduler:
except Exception as e:
logger.error(f'Ошибка обработки запланированных постов: {e}')
finally:
# Снимаем блокировку
self._scheduled_posts_lock = False

View file

@ -970,22 +970,29 @@ async def api_create_scheduled_post(
@app.get("/api/scheduled_posts/list")
async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
"""Список запланированных постов"""
from datetime import datetime
from datetime import timedelta
async with AsyncSessionLocal() as session:
stmt = select(ScheduledPost).order_by(ScheduledPost.scheduled_time.desc())
result = await session.execute(stmt)
posts = list(result.scalars().all())
# Часовой пояс пользователя (UTC+4 для Ульяновска)
user_tz_offset = timedelta(hours=4) # Europe/Ulyanovsk
posts_data = []
for post in posts:
# Конвертируем UTC в локальное время
local_time = post.scheduled_time + user_tz_offset
posts_data.append({
"id": post.id,
"text": post.text,
"topic_name": post.topic_name,
"topic_emoji": post.get_topic_emoji(),
"recipients": post.recipients,
"scheduled_time": post.scheduled_time.strftime('%d.%m.%Y %H:%M'),
"scheduled_time": local_time.strftime('%d.%m.%Y %H:%M'), # Локальное время!
"scheduled_time_utc": post.scheduled_time.strftime('%d.%m.%Y %H:%M'), # UTC для отладки
"status": post.status,
"status_emoji": post.get_status_emoji(),
"created_at": post.created_at.strftime('%d.%m.%Y %H:%M'),
@ -994,7 +1001,8 @@ async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
return JSONResponse({
"success": True,
"posts": posts_data
"posts": posts_data,
"timezone": "UTC+4 (Екатеринбург/Ульяновск)"
})

View file

@ -96,19 +96,20 @@
<div class="col-md-6 mb-3">
<label class="form-label">Дата публикации</label>
<input type="date" class="form-control" id="postDate" required>
<small class="text-muted">Когда опубликовать</small>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Время публикации</label>
<input type="time" class="form-control" id="postTime" required>
<small class="text-muted">Во сколько опубликовать</small>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<b>Информация:</b> Пост будет автоматически отправлен в выбранную тему форума в указанное время.
<b>Часовой пояс:</b> Все времена указаны в твоём локальном времени (UTC+4, Екатеринбург/Ульяновск).
<br>Бот автоматически конвертирует время в UTC для публикации по расписанию.
</div>
<button type="submit" class="btn btn-success btn-lg">
@ -140,8 +141,15 @@
<script>
// Установка минимальной даты (сегодня)
document.addEventListener('DOMContentLoaded', function() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('postDate').setAttribute('min', today);
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
document.getElementById('postDate').setAttribute('min', todayStr);
// Показываем текущее время в браузере пользователя
const tzOffset = -today.getTimezoneOffset() / 60;
const tzLabel = tzOffset >= 0 ? `UTC+${tzOffset}` : `UTC${tzOffset}`;
console.log(`🕐 Твой часовой пояс: ${tzLabel}`);
loadPosts();
});