nakama/backend/utils/tickets.js

222 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
});
}
module.exports = {
awardTickets,
awardPostCreation,
awardLikeGiven,
awardLikeReceived,
awardCommentWritten,
awardCommentReceived,
awardReferral,
awardArtLike,
awardArtComment,
canAwardTickets,
isAccountOldEnough
};