const User = require('../models/User'); const TicketActivity = require('../models/TicketActivity'); const Post = require('../models/Post'); const { getMoscowDate, formatMoscowTime } = require('./moscowTime'); /** * Проверяет, можно ли начислить баллы с учетом лимитов и антифрод правил */ async function canAwardTickets(userId, actionType, options = {}) { // Используем московское время для определения дня const today = getMoscowDate(); const activity = await TicketActivity.getOrCreateToday(userId); // Проверка лимитов в зависимости от типа действия switch (actionType) { case 'post_created': return activity.postsCreated < 5; case 'like_given': return activity.likesGiven < 50; case 'like_received': return activity.likesReceived < 100; case 'comment_written': // Проверка длины комментария (антифрод) if (options.commentLength && options.commentLength < 10) { return false; } return activity.commentsWritten < 20; case 'comment_received': return true; // Нет лимита на получение комментариев case 'referral': return activity.referralsCounted < 3; case 'art_reaction': // Проверка лимита на реакцию на арт (100 баллов в сутки с одного арта) const postId = options.postId; if (!postId) return false; const postIdStr = postId.toString(); const currentPoints = (activity.artReactionsPoints && activity.artReactionsPoints[postIdStr]) || 0; const newPoints = currentPoints + (options.points || 0); return newPoints <= 100; default: return false; } } /** * Проверяет возраст аккаунта (антифрод - аккаунты младше 24 часов не считаются) */ async function isAccountOldEnough(userId) { const user = await User.findById(userId); if (!user) return false; const accountAge = Date.now() - new Date(user.createdAt).getTime(); const hours24 = 24 * 60 * 60 * 1000; return accountAge >= hours24; } /** * Начисляет баллы пользователю с проверкой лимитов и антифрод */ async function awardTickets(userId, points, actionType, options = {}) { try { // Антифрод: проверка возраста аккаунта для входящих реакций if (actionType === 'like_received' || actionType === 'comment_received') { const senderId = options.senderId; if (senderId) { const senderOldEnough = await isAccountOldEnough(senderId); if (!senderOldEnough) { console.log(`[Tickets ${formatMoscowTime()}] Пропуск начисления: аккаунт отправителя младше 24 часов (${senderId})`); return { success: false, reason: 'account_too_new' }; } } } // Проверка возможности начисления const canAward = await canAwardTickets(userId, actionType, options); if (!canAward) { console.log(`[Tickets ${formatMoscowTime()}] Лимит достигнут для ${actionType} пользователя ${userId}`); return { success: false, reason: 'limit_reached' }; } // Получить активность за сегодня const activity = await TicketActivity.getOrCreateToday(userId); // Обновить счетчики активности switch (actionType) { case 'post_created': activity.postsCreated += 1; break; case 'like_given': activity.likesGiven += 1; break; case 'like_received': activity.likesReceived += 1; break; case 'comment_written': activity.commentsWritten += 1; break; case 'comment_received': activity.commentsReceived += 1; break; case 'referral': activity.referralsCounted += 1; break; case 'art_reaction': const postId = options.postId; if (postId) { if (!activity.artReactionsPoints) { activity.artReactionsPoints = {}; } const postIdStr = postId.toString(); const currentPoints = activity.artReactionsPoints[postIdStr] || 0; activity.artReactionsPoints[postIdStr] = currentPoints + points; activity.markModified('artReactionsPoints'); } break; } await activity.save(); // Начислить баллы пользователю await User.findByIdAndUpdate(userId, { $inc: { tickets: points } }); console.log(`[Tickets ${formatMoscowTime()}] Начислено ${points} баллов пользователю ${userId} за ${actionType}`); return { success: true, points }; } catch (error) { console.error(`[Tickets] Ошибка начисления баллов:`, error); return { success: false, reason: 'error', error: error.message }; } } /** * Начисляет баллы за создание поста */ async function awardPostCreation(userId) { return await awardTickets(userId, 15, 'post_created'); } /** * Начисляет баллы за лайк (тому, кто ставит) */ async function awardLikeGiven(userId) { return await awardTickets(userId, 1, 'like_given'); } /** * Начисляет баллы за полученный лайк (автору поста) */ async function awardLikeReceived(authorId, likerId) { return await awardTickets(authorId, 2, 'like_received', { senderId: likerId }); } /** * Начисляет баллы за написанный комментарий */ async function awardCommentWritten(userId, commentLength) { return await awardTickets(userId, 4, 'comment_written', { commentLength }); } /** * Начисляет баллы за полученный комментарий (автору поста) */ async function awardCommentReceived(authorId, commenterId) { return await awardTickets(authorId, 6, 'comment_received', { senderId: commenterId }); } /** * Начисляет баллы за реферала */ async function awardReferral(userId) { return await awardTickets(userId, 100, 'referral'); } /** * Начисляет баллы за реакцию на арт (лайк) */ async function awardArtLike(authorId, likerId, postId) { return await awardTickets(authorId, 8, 'art_reaction', { senderId: likerId, postId, points: 8 }); } /** * Начисляет баллы за комментарий под артом */ async function awardArtComment(authorId, commenterId, postId) { return await awardTickets(authorId, 12, 'art_reaction', { senderId: commenterId, postId, points: 12 }); } /** * Начисляет билеты за арт, прошедший модерацию */ async function awardArtModeration(userId) { // Проверяем лимиты: 1 арт в день / 5 в неделю const { getMoscowStartOfDay } = require('./moscowTime'); const today = getMoscowStartOfDay(); const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); const activity = await TicketActivity.getOrCreateToday(userId); // Проверка лимита: 1 арт в день const todayArts = activity.artsModerated || 0; if (todayArts >= 1) { console.log(`[Tickets ${formatMoscowTime()}] Лимит артов на сегодня достигнут для пользователя ${userId}`); return { success: false, reason: 'daily_limit_reached' }; } // Проверка лимита: 5 артов в неделю const TicketActivityModel = require('../models/TicketActivity'); const weekActivities = await TicketActivityModel.find({ user: userId, date: { $gte: weekAgo } }); const weekArtsCount = weekActivities.reduce((sum, act) => { return sum + (act.artsModerated || 0); }, 0); if (weekArtsCount >= 5) { console.log(`[Tickets ${formatMoscowTime()}] Лимит артов на неделю достигнут для пользователя ${userId}`); return { success: false, reason: 'weekly_limit_reached' }; } // Начислить билеты if (!activity.artsModerated) { activity.artsModerated = 0; } activity.artsModerated += 1; await activity.save(); await User.findByIdAndUpdate(userId, { $inc: { tickets: 40 } }); console.log(`[Tickets ${formatMoscowTime()}] Начислено 40 билетов пользователю ${userId} за арт, прошедший модерацию`); return { success: true, points: 40 }; } /** * Списывает билеты за модерацию арта при удалении поста */ async function deductArtModeration(userId, postCreatedAt) { try { const { getMoscowStartOfDay } = require('./moscowTime'); // Проверяем, что пост был создан сегодня const postDate = getMoscowStartOfDay(postCreatedAt); const today = getMoscowStartOfDay(); // Если пост создан не сегодня, не списываем билеты if (postDate.getTime() !== today.getTime()) { return { success: false, reason: 'post_not_today' }; } // Получить активность за сегодня const activity = await TicketActivity.getOrCreateToday(userId); // Уменьшить счетчик артов (но не меньше 0) if (activity.artsModerated > 0) { activity.artsModerated -= 1; await activity.save(); } // Списать билеты (но не меньше 0) const user = await User.findById(userId); if (user) { const ticketsToDeduct = Math.min(40, user.tickets || 0); if (ticketsToDeduct > 0) { await User.findByIdAndUpdate(userId, { $inc: { tickets: -ticketsToDeduct } }); console.log(`[Tickets ${formatMoscowTime()}] Списано ${ticketsToDeduct} билетов пользователю ${userId} за удаление арта`); return { success: true, points: -ticketsToDeduct }; } } return { success: true, points: 0 }; } catch (error) { console.error(`[Tickets] Ошибка списания билетов за арт:`, error); return { success: false, reason: 'error', error: error.message }; } } /** * Списывает билеты за действие (лайк/комментарий) с удаленным постом */ async function deductAction(userId, points, actionType, postCreatedAt) { try { const { getMoscowStartOfDay } = require('./moscowTime'); // Проверяем, что действие было в день создания поста const postDate = getMoscowStartOfDay(postCreatedAt); const today = getMoscowStartOfDay(); // Если пост создан не сегодня, не списываем билеты if (postDate.getTime() !== today.getTime()) { return { success: false, reason: 'post_not_today' }; } // Получить активность за сегодня const activity = await TicketActivity.getOrCreateToday(userId); // Уменьшить счетчики активности switch (actionType) { case 'post_created': if (activity.postsCreated > 0) { activity.postsCreated -= 1; } break; case 'like_given': if (activity.likesGiven > 0) { activity.likesGiven -= 1; } break; case 'like_received': if (activity.likesReceived > 0) { activity.likesReceived -= 1; } break; case 'comment_written': if (activity.commentsWritten > 0) { activity.commentsWritten -= 1; } break; case 'comment_received': if (activity.commentsReceived > 0) { activity.commentsReceived -= 1; } break; } await activity.save(); // Списать билеты (но не меньше 0) const user = await User.findById(userId); if (user) { const ticketsToDeduct = Math.min(points, user.tickets || 0); if (ticketsToDeduct > 0) { await User.findByIdAndUpdate(userId, { $inc: { tickets: -ticketsToDeduct } }); console.log(`[Tickets ${formatMoscowTime()}] Списано ${ticketsToDeduct} билетов пользователю ${userId} за ${actionType}`); return { success: true, points: -ticketsToDeduct }; } } return { success: true, points: 0 }; } catch (error) { console.error(`[Tickets] Ошибка списания билетов за действие:`, error); return { success: false, reason: 'error', error: error.message }; } } /** * Списывает все билеты, связанные с удаленным постом * Списывает только действия, которые были в день создания поста */ async function deductPostDeletion(post) { try { const { getMoscowStartOfDay } = require('./moscowTime'); // Проверяем, что пост был создан сегодня const postDate = getMoscowStartOfDay(post.createdAt); const today = getMoscowStartOfDay(); // Если пост создан не сегодня, не списываем билеты if (postDate.getTime() !== today.getTime()) { console.log(`[Tickets ${formatMoscowTime()}] Пост создан не сегодня, билеты не списываются`); return { success: false, reason: 'post_not_today' }; } const authorId = post.author; const postCreatedAt = post.createdAt; // 1. Списываем билеты за создание поста у автора await deductAction(authorId, 15, 'post_created', postCreatedAt); // 1.5. Если пост был помечен как арт, списываем билеты за модерацию арта if (post.isArt) { await deductArtModeration(authorId, postCreatedAt); } // 2. Списываем билеты за полученные лайки у автора if (post.likes && post.likes.length > 0) { for (const likerId of post.likes) { // Проверяем, что лайк был поставлен в день создания поста // (лайки не имеют даты, но если пост создан сегодня, то и лайки сегодня) await deductAction(authorId, 2, 'like_received', postCreatedAt); } } // 3. Списываем билеты за полученные комментарии у автора if (post.comments && post.comments.length > 0) { for (const comment of post.comments) { // Проверяем, что комментарий был написан в день создания поста const commentDate = getMoscowStartOfDay(comment.createdAt || comment.created_at || postCreatedAt); if (commentDate.getTime() === postDate.getTime()) { await deductAction(authorId, 6, 'comment_received', postCreatedAt); } } } // 4. Списываем билеты у тех, кто поставил лайк (за поставленный лайк) if (post.likes && post.likes.length > 0) { for (const likerId of post.likes) { // Проверяем, что это не автор поста if (!likerId.equals(authorId)) { await deductAction(likerId, 1, 'like_given', postCreatedAt); } } } // 5. Списываем билеты у тех, кто написал комментарий (за написанный комментарий) if (post.comments && post.comments.length > 0) { for (const comment of post.comments) { const commentAuthorId = comment.author; if (commentAuthorId && !commentAuthorId.equals(authorId)) { // Проверяем, что комментарий был написан в день создания поста const commentDate = getMoscowStartOfDay(comment.createdAt || comment.created_at || postCreatedAt); if (commentDate.getTime() === postDate.getTime()) { // Проверяем длину комментария (>= 10 символов для начисления) const commentLength = (comment.content || '').trim().length; if (commentLength >= 10) { await deductAction(commentAuthorId, 4, 'comment_written', postCreatedAt); } } } } } console.log(`[Tickets ${formatMoscowTime()}] Списаны все билеты за удаление поста ${post._id}`); return { success: true }; } catch (error) { console.error(`[Tickets] Ошибка списания билетов за удаление поста:`, error); return { success: false, reason: 'error', error: error.message }; } } /** * Списывает билеты при удалении поста (старая функция для обратной совместимости) * @deprecated Используйте deductPostDeletion вместо этого */ async function deductPostCreation(userId, postCreatedAt) { return await deductAction(userId, 15, 'post_created', postCreatedAt); } module.exports = { awardTickets, awardPostCreation, awardLikeGiven, awardLikeReceived, awardCommentWritten, awardCommentReceived, awardReferral, awardArtLike, awardArtComment, awardArtModeration, deductPostCreation, deductPostDeletion, deductAction, canAwardTickets, isAccountOldEnough };