diff --git a/backend/models/Post.js b/backend/models/Post.js
index bbf42df..4c64452 100644
--- a/backend/models/Post.js
+++ b/backend/models/Post.js
@@ -56,6 +56,10 @@ const PostSchema = new mongoose.Schema({
type: Boolean,
default: false
},
+ isArt: {
+ type: Boolean,
+ default: false
+ },
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
diff --git a/backend/models/TicketActivity.js b/backend/models/TicketActivity.js
index 99c1340..74b20c4 100644
--- a/backend/models/TicketActivity.js
+++ b/backend/models/TicketActivity.js
@@ -43,6 +43,7 @@ const TicketActivitySchema = new mongoose.Schema({
type: mongoose.Schema.Types.Mixed,
default: {}
},
+ artsModerated: { type: Number, default: 0 }, // Количество артов, прошедших модерацию
createdAt: {
type: Date,
default: Date.now
diff --git a/backend/routes/modApp.js b/backend/routes/modApp.js
index 4b6ca51..3c6fa50 100644
--- a/backend/routes/modApp.js
+++ b/backend/routes/modApp.js
@@ -173,6 +173,7 @@ router.get('/posts', authenticateModeration, requireModerationAccess, async (req
commentsCount: post.comments?.length || 0,
likesCount: post.likes?.length || 0,
isNSFW: post.isNSFW,
+ isArt: post.isArt,
publishedToChannel: post.publishedToChannel,
adminNumber: post.adminNumber,
editedAt: post.editedAt,
@@ -245,7 +246,7 @@ router.delete('/posts/:postId/comments/:commentId', authenticateModeration, requ
});
router.put('/posts/:id', authenticateModeration, requireModerationAccess, async (req, res) => {
- const { content, hashtags, tags, isNSFW } = req.body;
+ const { content, hashtags, tags, isNSFW, isArt } = req.body;
const post = await Post.findById(req.params.id).populate('author');
if (!post) {
@@ -285,6 +286,18 @@ router.put('/posts/:id', authenticateModeration, requireModerationAccess, async
post.isNSFW = !!isNSFW;
}
+ // Обработка метки "арт"
+ const wasArt = post.isArt || false;
+ if (isArt !== undefined) {
+ post.isArt = !!isArt;
+
+ // Если арт только что помечен (было false, стало true), начислить билеты
+ if (!wasArt && post.isArt && post.author) {
+ const { awardArtModeration } = require('../utils/tickets');
+ await awardArtModeration(post.author._id);
+ }
+ }
+
post.editedAt = new Date();
await post.save();
@@ -312,6 +325,7 @@ router.put('/posts/:id', authenticateModeration, requireModerationAccess, async
tags: post.tags,
images: post.images,
isNSFW: post.isNSFW,
+ isArt: post.isArt,
publishedToChannel: post.publishedToChannel,
adminNumber: post.adminNumber,
editedAt: post.editedAt,
diff --git a/backend/utils/tickets.js b/backend/utils/tickets.js
index 1e132c0..3b02e25 100644
--- a/backend/utils/tickets.js
+++ b/backend/utils/tickets.js
@@ -205,6 +205,55 @@ async function awardArtComment(authorId, commenterId, postId) {
});
}
+/**
+ * Начисляет билеты за арт, прошедший модерацию
+ */
+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 };
+}
+
/**
* Списывает билеты за действие (лайк/комментарий) с удаленным постом
*/
@@ -373,6 +422,7 @@ module.exports = {
awardReferral,
awardArtLike,
awardArtComment,
+ awardArtModeration,
deductPostCreation,
deductPostDeletion,
deductAction,
diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx
index 00d7315..e2be27d 100644
--- a/moderation/frontend/src/App.jsx
+++ b/moderation/frontend/src/App.jsx
@@ -422,6 +422,12 @@ export default function App() {
loadUsers();
};
+ const handleToggleArt = async (post) => {
+ const newIsArt = !post.isArt;
+ await updatePost(post.id, { isArt: newIsArt });
+ loadPosts();
+ };
+
const handleOpenComments = async (postId) => {
setCommentsLoading(true);
try {
@@ -631,6 +637,13 @@ export default function App() {
Редактировать
+