From dc66c8041c4459e4dfe09059f2828d4e56d5abdb Mon Sep 17 00:00:00 2001 From: Professional Date: Sat, 24 May 2025 02:10:15 +0700 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/userController.js | 75 ++++++++++++++++++++++++--- backend/models/ProfileView.js | 38 ++++++++++++++ backend/routes/userRoutes.js | 6 ++- src/services/api.js | 5 ++ src/views/SwipeView.vue | 14 +++++ src/views/UserProfileView.vue | 26 +++++----- 6 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 backend/models/ProfileView.js diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js index b10e5b1..537de84 100644 --- a/backend/controllers/userController.js +++ b/backend/controllers/userController.js @@ -1,6 +1,7 @@ const User = require('../models/User'); const cloudinary = require('../config/cloudinaryConfig'); // Импортируем настроенный Cloudinary SDK const path = require('path'); // Может понадобиться для получения расширения файла +const ProfileView = require('../models/ProfileView'); // @desc Обновить профиль пользователя // @route PUT /api/users/profile @@ -140,9 +141,7 @@ const getUsersForSwiping = async (req, res, next) => { gender: user.gender, bio: user.bio, mainPhotoUrl: mainPhotoUrl, // <--- Добавляем URL главной фотографии - // photos: user.photos, // Отправлять весь массив фото здесь, возможно, избыточно для карточки свайпа - // Если нужна только одна фото, то mainPhotoUrl достаточно. - // Если нужны несколько превью - можно вернуть ограниченное количество. + photos: user.photos, // Возвращаем все фото для карусели }; }); @@ -393,6 +392,65 @@ const getUserById = async (req, res, next) => { } }; +// @desc Записать просмотр профиля пользователя +// @route POST /api/users/:userId/view +// @access Private +const recordProfileView = async (req, res, next) => { + try { + const { userId } = req.params; + const viewerId = req.user._id; + const { source = 'other' } = req.body; + + // Проверяем, что пользователь не пытается просматривать свой собственный профиль + if (viewerId.equals(userId)) { + return res.status(200).json({ message: 'Просмотр собственного профиля не записывается' }); + } + + // Проверяем, существует ли пользователь, чей профиль просматривают + const profileOwner = await User.findById(userId); + if (!profileOwner) { + const error = new Error('Пользователь не найден.'); + error.statusCode = 404; + return next(error); + } + + // Проверяем, был ли уже просмотр от этого пользователя в последние 5 минут + // Это предотвращает спам просмотров при обновлении страницы + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + const recentView = await ProfileView.findOne({ + viewer: viewerId, + profileOwner: userId, + viewedAt: { $gte: fiveMinutesAgo } + }); + + if (recentView) { + console.log(`[USER_CTRL] Недавний просмотр уже существует для пользователя ${viewerId} -> ${userId}`); + return res.status(200).json({ message: 'Просмотр уже записан' }); + } + + // Создаем новую запись просмотра + const newView = new ProfileView({ + viewer: viewerId, + profileOwner: userId, + source: source, + viewedAt: new Date() + }); + + await newView.save(); + + console.log(`[USER_CTRL] Записан просмотр профиля ${userId} пользователем ${viewerId}, источник: ${source}`); + + res.status(200).json({ + message: 'Просмотр профиля записан', + viewId: newView._id + }); + + } catch (error) { + console.error('[USER_CTRL] Ошибка при записи просмотра профиля:', error.message); + next(error); + } +}; + // @desc Получить статистику пользователя для главной страницы // @route GET /api/users/stats // @access Private @@ -428,10 +486,10 @@ const getUserStats = async (req, res, next) => { readBy: { $ne: currentUserId } // Которые текущий пользователь не читал }); - // 4. Для просмотров профиля пока возвращаем заглушку, - // так как у нас нет модели для отслеживания просмотров - // В будущем можно добавить отдельную коллекцию ProfileViews - const profileViewsCount = 0; // TODO: Реализовать отслеживание просмотров + // 4. Получаем количество просмотров профиля + const profileViewsCount = await ProfileView.countDocuments({ + profileOwner: currentUserId + }); const stats = { profileViews: profileViewsCount, @@ -457,5 +515,6 @@ module.exports = { setMainPhoto, deletePhoto, getUserById, - getUserStats, // <--- Добавляем новую функцию + getUserStats, + recordProfileView, // <--- Добавляем новую функцию }; \ No newline at end of file diff --git a/backend/models/ProfileView.js b/backend/models/ProfileView.js new file mode 100644 index 0000000..ac78365 --- /dev/null +++ b/backend/models/ProfileView.js @@ -0,0 +1,38 @@ +const mongoose = require('mongoose'); + +const profileViewSchema = new mongoose.Schema({ + // Кто просматривал + viewer: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + // Чей профиль просматривали + profileOwner: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + // Дата и время просмотра + viewedAt: { + type: Date, + default: Date.now + }, + // Источник просмотра (например, 'swipe', 'profile_link', 'chat', 'match') + source: { + type: String, + enum: ['swipe', 'profile_link', 'chat', 'match', 'other'], + default: 'other' + } +}, { + timestamps: true +}); + +// Составной индекс для предотвращения дублирования просмотров от одного пользователя в короткий период времени +// и для быстрого поиска просмотров конкретного профиля +profileViewSchema.index({ viewer: 1, profileOwner: 1, viewedAt: 1 }); + +// Индекс для быстрого подсчета просмотров конкретного пользователя +profileViewSchema.index({ profileOwner: 1, viewedAt: -1 }); + +module.exports = mongoose.model('ProfileView', profileViewSchema); \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 9dc29d7..fe608c8 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto, setMainPhoto, deletePhoto, getUserById, getUserStats } = require('../controllers/userController'); +const { updateUserProfile, getUsersForSwiping, uploadUserProfilePhoto, setMainPhoto, deletePhoto, getUserById, getUserStats, recordProfileView } = require('../controllers/userController'); const { protect } = require('../middleware/authMiddleware'); // Нам нужен protect для защиты маршрута const multer = require('multer'); // 1. Импортируем multer const path = require('path'); // Может понадобиться для фильтрации файлов @@ -46,6 +46,10 @@ router.post('/profile/photo', protect, upload.single('profilePhoto'), uploadUser router.put('/profile/photo/:photoId/set-main', protect, setMainPhoto); // <--- НОВЫЙ МАРШРУТ router.delete('/profile/photo/:photoId', protect, deletePhoto); // <--- НОВЫЙ МАРШРУТ +// Маршрут для записи просмотра профиля +// POST /api/users/:userId/view +router.post('/:userId/view', protect, recordProfileView); // <--- НОВЫЙ МАРШРУТ + // Маршрут для получения профиля по ID (например, для просмотра чужих профилей, если это нужно) // GET /api/users/:id router.get('/:userId', protect, getUserById); // <--- НОВЫЙ МАРШРУТ diff --git a/src/services/api.js b/src/services/api.js index 9a7dcaa..d67bbc8 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -116,5 +116,10 @@ export default { // Новый метод для получения статистики пользователя getUserStats() { return apiClient.get('/users/stats'); + }, + + // Новый метод для записи просмотра профиля + recordProfileView(userId, source = 'other') { + return apiClient.post(`/users/${userId}/view`, { source }); } }; \ No newline at end of file diff --git a/src/views/SwipeView.vue b/src/views/SwipeView.vue index 08f1122..4b13ddd 100644 --- a/src/views/SwipeView.vue +++ b/src/views/SwipeView.vue @@ -741,6 +741,20 @@ onUnmounted(() => { watch(suggestions, () => { initPhotoIndices(); }, { deep: true }); + +// Следим за изменением текущего пользователя для записи просмотров +watch(currentUserToSwipe, async (newUser, oldUser) => { + if (newUser && newUser._id && (!oldUser || newUser._id !== oldUser._id)) { + // Записываем просмотр профиля для нового пользователя + try { + await api.recordProfileView(newUser._id, 'swipe'); + console.log('[SwipeView] Просмотр профиля записан для пользователя:', newUser.name, newUser._id); + } catch (viewError) { + console.warn('[SwipeView] Не удалось записать просмотр профиля:', viewError); + // Не показываем ошибку пользователю, так как это не критично + } + } +});