2025-12-07 02:20:45 +00:00
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 03:08:07 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Списывает билеты за действие (лайк/комментарий) с удаленным постом
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 02:20:45 +00:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
awardTickets,
|
|
|
|
|
|
awardPostCreation,
|
|
|
|
|
|
awardLikeGiven,
|
|
|
|
|
|
awardLikeReceived,
|
|
|
|
|
|
awardCommentWritten,
|
|
|
|
|
|
awardCommentReceived,
|
|
|
|
|
|
awardReferral,
|
|
|
|
|
|
awardArtLike,
|
|
|
|
|
|
awardArtComment,
|
2025-12-07 03:08:07 +00:00
|
|
|
|
deductPostCreation,
|
|
|
|
|
|
deductPostDeletion,
|
|
|
|
|
|
deductAction,
|
2025-12-07 02:20:45 +00:00
|
|
|
|
canAwardTickets,
|
|
|
|
|
|
isAccountOldEnough
|
|
|
|
|
|
};
|
|
|
|
|
|
|