Fix like/dislike errors and implement native chat system

This commit is contained in:
2025-09-13 07:51:02 +09:00
parent 8893b4ad22
commit 321547bf27
14 changed files with 1236 additions and 39 deletions

View File

@@ -44,7 +44,7 @@ export class MatchingService {
// Получить профили пользователей
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
const targetProfile = await this.profileService.getProfileByUserId(targetTelegramId); if (!userProfile || !targetProfile) {
const targetProfile = await this.profileService.getProfileByTelegramId(targetTelegramId); if (!userProfile || !targetProfile) {
throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 400);
}
@@ -82,26 +82,37 @@ export class MatchingService {
`, [targetUserId, userId]);
if (reciprocalSwipe.rows.length > 0) {
isMatch = true;
const matchId = uuidv4();
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
// Проверяем, что матч еще не существует
const existingMatch = await client.query(`
SELECT * FROM matches
WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1)
`, [userId, targetUserId]);
// Создаем матч
await client.query(`
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
VALUES ($1, $2, $3, $4, $5)
`, [matchId, userId, targetUserId, new Date(), 'active']);
if (existingMatch.rows.length === 0) {
isMatch = true;
const matchId = uuidv4();
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
match = new Match({
id: matchId,
userId1: userId,
userId2: targetUserId,
createdAt: new Date(),
isActive: true,
isSuperMatch: false,
unreadCount1: 0,
unreadCount2: 0
});
// Упорядочиваем пользователей для консистентности
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
// Создаем матч
await client.query(`
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
VALUES ($1, $2, $3, $4, $5)
`, [matchId, user1Id, user2Id, new Date(), 'active']);
match = new Match({
id: matchId,
userId1: user1Id,
userId2: user2Id,
createdAt: new Date(),
isActive: true,
isSuperMatch: isSuperMatch,
unreadCount1: 0,
unreadCount2: 0
});
}
}
}
});

View File

@@ -227,15 +227,65 @@ export class NotificationService {
// Проверить, активен ли пользователь в чате
private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise<boolean> {
// TODO: Реализовать проверку активности пользователя
// Можно использовать Redis для хранения состояния активности
return false;
try {
// Проверяем последнее сообщение пользователя в чате
const result = await query(`
SELECT m.created_at
FROM messages m
JOIN matches mt ON m.match_id = mt.id
WHERE (mt.user1_id = $1 OR mt.user2_id = $1)
AND (mt.user1_id = $2 OR mt.user2_id = $2)
AND m.sender_id = $1
ORDER BY m.created_at DESC
LIMIT 1
`, [userId, chatWithUserId]);
if (result.rows.length === 0) {
return false; // Нет сообщений - не активен
}
const lastMessageTime = new Date(result.rows[0].created_at);
const now = new Date();
const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60);
// Считаем активным если последнее сообщение было менее 24 часов назад
return hoursSinceLastMessage < 24;
} catch (error) {
console.error('Error checking user activity:', error);
return false;
}
}
// Отправить пуш-уведомление (для будущего использования)
async sendPushNotification(userId: string, title: string, body: string, data?: any): Promise<void> {
// TODO: Интеграция с Firebase Cloud Messaging или другим сервисом пуш-уведомлений
console.log(`Push notification for ${userId}: ${title} - ${body}`);
try {
// Логируем уведомление
console.log(`📱 Push notification prepared for user ${userId}:`);
console.log(`📋 Title: ${title}`);
console.log(`💬 Body: ${body}`);
if (data) {
console.log(`📊 Data:`, JSON.stringify(data, null, 2));
}
// В будущем здесь будет интеграция с Firebase Cloud Messaging
// или другим сервисом пуш-уведомлений:
/*
const message = {
notification: {
title,
body
},
data: data ? JSON.stringify(data) : undefined,
token: await this.getUserPushToken(userId)
};
await admin.messaging().send(message);
console.log(`✅ Push notification sent to user ${userId}`);
*/
} catch (error) {
console.error(`❌ Error preparing push notification for user ${userId}:`, error);
}
}
// Получить настройки уведомлений пользователя
@@ -300,7 +350,7 @@ export class NotificationService {
// Получаем запланированные уведомления
const result = await query(`
SELECT * FROM scheduled_notifications
WHERE scheduled_at <= $1 AND sent = false
WHERE scheduled_at <= $1 AND processed = false
ORDER BY scheduled_at ASC
LIMIT 100
`, [new Date()]);
@@ -318,10 +368,10 @@ export class NotificationService {
// Добавить другие типы уведомлений
}
// Отмечаем как отправленное
// Отмечаем как обработанное
await query(
'UPDATE scheduled_notifications SET sent = true, sent_at = $1 WHERE id = $2',
[new Date(), notification.id]
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
[notification.id]
);
} catch (error) {
console.error(`Error processing notification ${notification.id}:`, error);

View File

@@ -95,7 +95,18 @@ export class ProfileService {
}
return this.mapEntityToProfile(result.rows[0]);
} // Получение UUID пользователя по Telegram ID
}
// Получение Telegram ID по UUID пользователя
async getTelegramIdByUserId(userId: string): Promise<string | null> {
const result = await query(`
SELECT telegram_id FROM users WHERE id = $1
`, [userId]);
return result.rows.length > 0 ? result.rows[0].telegram_id.toString() : null;
}
// Получение UUID пользователя по Telegram ID
async getUserIdByTelegramId(telegramId: string): Promise<string | null> {
const result = await query(`
SELECT id FROM users WHERE telegram_id = $1
@@ -162,7 +173,8 @@ export class ProfileService {
case 'photos':
case 'interests':
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
updateValues.push(JSON.stringify(value));
// Для PostgreSQL массивы передаем как есть, не как JSON строки
updateValues.push(value);
break;
case 'location':
if (value && typeof value === 'object' && 'latitude' in value) {
@@ -336,7 +348,7 @@ export class ProfileService {
return {
totalLikes: parseInt(likesResult.rows[0].count),
totalMatches: parseInt(matchesResult.rows[0].count),
profileViews: 0, // TODO: implement profile views tracking
profileViews: await this.getProfileViewsCount(userId),
likesReceived: parseInt(likesReceivedResult.rows[0].count)
};
}
@@ -499,4 +511,61 @@ export class ProfileService {
profile.isVisible = newVisibility;
return profile;
}
// Записать просмотр профиля
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
try {
await query(`
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type)
VALUES (
(SELECT id FROM users WHERE telegram_id = $1),
(SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)),
$3
)
ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE
SET viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type
`, [viewerId, viewedProfileId, viewType]);
} catch (error) {
console.error('Error recording profile view:', error);
}
}
// Получить количество просмотров профиля
async getProfileViewsCount(userId: string): Promise<number> {
try {
const result = await query(`
SELECT COUNT(*) as count
FROM profile_views pv
JOIN profiles p ON pv.viewed_profile_id = p.id
WHERE p.user_id = $1
`, [userId]);
return parseInt(result.rows[0].count) || 0;
} catch (error) {
console.error('Error getting profile views count:', error);
return 0;
}
}
// Получить список кто просматривал профиль
async getProfileViewers(userId: string, limit: number = 10): Promise<Profile[]> {
try {
const result = await query(`
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
FROM profile_views pv
JOIN profiles target_p ON pv.viewed_profile_id = target_p.id
JOIN users viewer_u ON pv.viewer_id = viewer_u.id
JOIN profiles p ON viewer_u.id = p.user_id
JOIN users u ON p.user_id = u.id
WHERE target_p.user_id = $1
ORDER BY pv.viewed_at DESC
LIMIT $2
`, [userId, limit]);
return result.rows.map((row: any) => this.mapEntityToProfile(row));
} catch (error) {
console.error('Error getting profile viewers:', error);
return [];
}
}
}