mass refactor

This commit is contained in:
2025-09-18 08:31:14 +09:00
parent 856bf3ca2a
commit bdd7d0424f
58 changed files with 3009 additions and 291 deletions

View File

@@ -24,8 +24,8 @@ export class ChatService {
SELECT
m.*,
CASE
WHEN m.user1_id = $1 THEN m.user2_id
ELSE m.user1_id
WHEN m.user_id_1 = $1 THEN m.user_id_2
ELSE m.user_id_1
END as other_user_id,
p.name as other_user_name,
p.photos as other_user_photos,
@@ -42,8 +42,8 @@ export class ChatService {
FROM matches m
LEFT JOIN profiles p ON (
CASE
WHEN m.user1_id = $1 THEN p.user_id = m.user2_id
ELSE p.user_id = m.user1_id
WHEN m.user_id_1 = $1 THEN p.user_id = m.user_id_2
ELSE p.user_id = m.user_id_1
END
)
LEFT JOIN messages msg ON msg.id = (
@@ -52,10 +52,10 @@ export class ChatService {
ORDER BY created_at DESC
LIMIT 1
)
WHERE (m.user1_id = $1 OR m.user2_id = $1)
AND m.status = 'active'
WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1)
AND m.is_active = true
ORDER BY
CASE WHEN msg.created_at IS NULL THEN m.matched_at ELSE msg.created_at END DESC
CASE WHEN msg.created_at IS NULL THEN m.created_at ELSE msg.created_at END DESC
`, [userId]);
return result.rows.map((row: any) => ({
@@ -91,7 +91,6 @@ export class ChatService {
senderId: row.sender_id,
content: row.content,
messageType: row.message_type,
fileId: row.file_id,
isRead: row.is_read,
createdAt: new Date(row.created_at)
})).reverse(); // Возвращаем в хронологическом порядке
@@ -106,8 +105,7 @@ export class ChatService {
matchId: string,
senderTelegramId: string,
content: string,
messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text',
fileId?: string
messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text'
): Promise<Message | null> {
try {
// Получаем senderId по telegramId
@@ -119,7 +117,7 @@ export class ChatService {
// Проверяем, что матч активен и пользователь является участником
const matchResult = await query(`
SELECT * FROM matches
WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active'
WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true
`, [matchId, senderId]);
if (matchResult.rows.length === 0) {
@@ -130,9 +128,9 @@ export class ChatService {
// Создаем сообщение
await query(`
INSERT INTO messages (id, match_id, sender_id, content, message_type, file_id, is_read, created_at)
VALUES ($1, $2, $3, $4, $5, $6, false, CURRENT_TIMESTAMP)
`, [messageId, matchId, senderId, content, messageType, fileId]);
INSERT INTO messages (id, match_id, sender_id, content, message_type, is_read, created_at)
VALUES ($1, $2, $3, $4, $5, false, CURRENT_TIMESTAMP)
`, [messageId, matchId, senderId, content, messageType]);
// Обновляем время последнего сообщения в матче
await query(`
@@ -157,7 +155,6 @@ export class ChatService {
senderId: row.sender_id,
content: row.content,
messageType: row.message_type,
fileId: row.file_id,
isRead: row.is_read,
createdAt: new Date(row.created_at)
});
@@ -197,11 +194,11 @@ export class ChatService {
SELECT
m.*,
CASE
WHEN m.user1_id = $2 THEN m.user2_id
ELSE m.user1_id
WHEN m.user_id_1 = $2 THEN m.user_id_2
ELSE m.user_id_1
END as other_user_id
FROM matches m
WHERE m.id = $1 AND (m.user1_id = $2 OR m.user2_id = $2) AND m.status = 'active'
WHERE m.id = $1 AND (m.user_id_1 = $2 OR m.user_id_2 = $2) AND m.is_active = true
`, [matchId, userId]);
if (result.rows.length === 0) {
@@ -234,7 +231,7 @@ export class ChatService {
// Проверяем, что пользователь является участником матча
const matchResult = await query(`
SELECT * FROM matches
WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active'
WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true
`, [matchId, userId]);
if (matchResult.rows.length === 0) {
@@ -244,9 +241,11 @@ export class ChatService {
// Помечаем матч как неактивный
await query(`
UPDATE matches
SET status = 'unmatched'
SET is_active = false,
unmatched_at = NOW(),
unmatched_by = $2
WHERE id = $1
`, [matchId]);
`, [matchId, userId]);
return true;
} catch (error) {

View File

@@ -70,7 +70,7 @@ export class MatchingService {
await transaction(async (client) => {
// Создаем свайп
await client.query(`
INSERT INTO swipes (id, swiper_id, swiped_id, direction, created_at)
INSERT INTO swipes (id, user_id, target_user_id, direction, created_at)
VALUES ($1, $2, $3, $4, $5)
`, [swipeId, userId, targetUserId, direction, new Date()]);
@@ -78,14 +78,14 @@ export class MatchingService {
if (swipeType === 'like' || swipeType === 'superlike') {
const reciprocalSwipe = await client.query(`
SELECT * FROM swipes
WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('like', 'super')
WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super')
`, [targetUserId, userId]);
if (reciprocalSwipe.rows.length > 0) {
// Проверяем, что матч еще не существует
const existingMatch = await client.query(`
SELECT * FROM matches
WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1)
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
`, [userId, targetUserId]);
if (existingMatch.rows.length === 0) {
@@ -98,9 +98,9 @@ export class MatchingService {
// Создаем матч
await client.query(`
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active)
VALUES ($1, $2, $3, $4, $5)
`, [matchId, user1Id, user2Id, new Date(), 'active']);
`, [matchId, user1Id, user2Id, new Date(), true]);
match = new Match({
id: matchId,
@@ -143,7 +143,7 @@ export class MatchingService {
async getSwipe(userId: string, targetUserId: string): Promise<Swipe | null> {
const result = await query(`
SELECT * FROM swipes
WHERE swiper_id = $1 AND swiped_id = $2
WHERE user_id = $1 AND target_user_id = $2
`, [userId, targetUserId]);
if (result.rows.length === 0) {
@@ -163,8 +163,8 @@ export class MatchingService {
const result = await query(`
SELECT * FROM matches
WHERE (user1_id = $1 OR user2_id = $1) AND status = 'active'
ORDER BY matched_at DESC
WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true
ORDER BY created_at DESC
LIMIT $2
`, [userId, limit]);
@@ -217,7 +217,7 @@ export class MatchingService {
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
const result = await query(`
SELECT * FROM swipes
WHERE swiped_id = $1 AND direction IN ('like', 'super') AND is_match = false
WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false
ORDER BY created_at DESC
LIMIT $2
`, [userId, limit]);
@@ -311,11 +311,11 @@ export class MatchingService {
private mapEntityToMatch(entity: any): Match {
return new Match({
id: entity.id,
userId1: entity.user1_id,
userId2: entity.user2_id,
createdAt: entity.matched_at || entity.created_at,
userId1: entity.user_id_1,
userId2: entity.user_id_2,
createdAt: entity.created_at,
lastMessageAt: entity.last_message_at,
isActive: entity.status === 'active',
isActive: entity.is_active === true,
isSuperMatch: false, // Определяется из swipes если нужно
unreadCount1: 0,
unreadCount2: 0
@@ -329,8 +329,8 @@ export class MatchingService {
FROM swipes s1
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
WHERE s1.user_id = $1
AND s1.type IN ('like', 'superlike')
AND s2.type IN ('like', 'superlike')
AND s1.direction IN ('right', 'super')
AND s2.direction IN ('right', 'super')
AND NOT EXISTS (
SELECT 1 FROM matches m
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
@@ -342,7 +342,7 @@ export class MatchingService {
}
// Получить следующего кандидата для просмотра
async getNextCandidate(telegramId: string): Promise<Profile | null> {
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
// Сначала получаем профиль пользователя по telegramId
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
if (!userProfile) {
@@ -354,18 +354,26 @@ export class MatchingService {
// Получаем список уже просмотренных пользователей
const viewedUsers = await query(`
SELECT DISTINCT swiped_id
SELECT DISTINCT target_user_id
FROM swipes
WHERE swiper_id = $1
WHERE user_id = $1
`, [userId]);
const viewedUserIds = viewedUsers.rows.map((row: any) => row.swiped_id);
const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id);
viewedUserIds.push(userId); // Исключаем самого себя
// Формируем условие для исключения уже просмотренных
const excludeCondition = viewedUserIds.length > 0
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
: '';
// Если это новый пользователь или у пользователя мало просмотренных профилей,
// показываем всех пользователей по очереди (исключая только себя)
let excludeCondition = '';
if (!isNewUser) {
excludeCondition = viewedUserIds.length > 0
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
: '';
} else {
// Для новых пользователей исключаем только себя
excludeCondition = `AND p.user_id != $2`;
}
// Ищем подходящих кандидатов
const candidateQuery = `

View File

@@ -233,8 +233,8 @@ export class NotificationService {
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)
WHERE (mt.user_id_1 = $1 OR mt.user_id_2 = $1)
AND (mt.user_id_1 = $2 OR mt.user_id_2 = $2)
AND m.sender_id = $1
ORDER BY m.created_at DESC
LIMIT 1
@@ -347,10 +347,33 @@ export class NotificationService {
// Планировщик уведомлений (вызывается периодически)
async processScheduledNotifications(): Promise<void> {
try {
// Проверим, существует ли таблица scheduled_notifications
const tableCheck = await query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'scheduled_notifications'
) as exists
`);
if (!tableCheck.rows[0].exists) {
// Если таблицы нет, создаем её
await query(`
CREATE TABLE IF NOT EXISTS scheduled_notifications (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
type VARCHAR(50) NOT NULL,
data JSONB,
scheduled_at TIMESTAMP NOT NULL,
is_processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
}
// Получаем запланированные уведомления
const result = await query(`
SELECT * FROM scheduled_notifications
WHERE scheduled_at <= $1 AND processed = false
WHERE scheduled_at <= $1 AND is_processed = false
ORDER BY scheduled_at ASC
LIMIT 100
`, [new Date()]);
@@ -370,7 +393,7 @@ export class NotificationService {
// Отмечаем как обработанное
await query(
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1',
[notification.id]
);
} catch (error) {

View File

@@ -49,18 +49,15 @@ export class ProfileService {
// Сохранение в базу данных
await query(`
INSERT INTO profiles (
id, user_id, name, age, gender, looking_for, bio, photos, interests,
hobbies, location, education, occupation, height, religion, dating_goal,
latitude, longitude, verification_status, is_active, is_visible,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
id, user_id, name, age, gender, interested_in, bio, photos,
city, education, job, height, religion, dating_goal,
is_verified, is_visible, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`, [
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
profile.bio, profile.photos, profile.interests, profile.hobbies,
profile.city, profile.education, profile.job, profile.height,
profile.religion, profile.datingGoal,
profile.location?.latitude, profile.location?.longitude,
'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
profile.height, profile.religion, profile.datingGoal,
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
]);
return profile;
@@ -137,8 +134,7 @@ export class ProfileService {
ON CONFLICT (telegram_id) DO UPDATE SET
username = EXCLUDED.username,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
updated_at = CURRENT_TIMESTAMP
last_name = EXCLUDED.last_name
RETURNING id
`, [
parseInt(telegramId),
@@ -177,12 +173,8 @@ export class ProfileService {
updateValues.push(value);
break;
case 'location':
if (value && typeof value === 'object' && 'latitude' in value) {
updateFields.push(`latitude = $${paramIndex++}`);
updateValues.push(value.latitude);
updateFields.push(`longitude = $${paramIndex++}`);
updateValues.push(value.longitude);
}
// Пропускаем обработку местоположения, так как колонки location нет
console.log('Skipping location update - column does not exist');
break;
case 'searchPreferences':
// Поля search preferences больше не хранятся в БД, пропускаем
@@ -339,8 +331,8 @@ export class ProfileService {
const [likesResult, matchesResult, likesReceivedResult] = await Promise.all([
query('SELECT COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND direction IN ($2, $3)',
[userId, 'like', 'super']),
query('SELECT COUNT(*) as count FROM matches WHERE (user1_id = $1 OR user2_id = $1) AND status = $2',
[userId, 'active']),
query('SELECT COUNT(*) as count FROM matches WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true',
[userId]),
query('SELECT COUNT(*) as count FROM swipes WHERE swiped_id = $1 AND direction IN ($2, $3)',
[userId, 'like', 'super'])
]);
@@ -424,6 +416,27 @@ export class ProfileService {
return [];
};
// Функция для парсинга JSON полей
const parseJsonField = (jsonField: any): any[] => {
if (!jsonField) return [];
// Если это строка, пробуем распарсить JSON
if (typeof jsonField === 'string') {
try {
const parsed = JSON.parse(jsonField);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('Error parsing JSON field:', e);
return [];
}
}
// Если это уже массив, возвращаем как есть
if (Array.isArray(jsonField)) return jsonField;
return [];
};
return new Profile({
userId: entity.user_id,
name: entity.name,
@@ -431,8 +444,8 @@ export class ProfileService {
gender: entity.gender,
interestedIn: entity.looking_for,
bio: entity.bio,
photos: parsePostgresArray(entity.photos),
interests: parsePostgresArray(entity.interests),
photos: parseJsonField(entity.photos),
interests: parseJsonField(entity.interests),
hobbies: entity.hobbies,
city: entity.location || entity.city,
education: entity.education,
@@ -441,14 +454,11 @@ export class ProfileService {
religion: entity.religion,
datingGoal: entity.dating_goal,
lifestyle: {
smoking: entity.smoking,
drinking: entity.drinking,
kids: entity.has_kids
},
location: entity.latitude && entity.longitude ? {
latitude: entity.latitude,
longitude: entity.longitude
} : undefined,
smoking: undefined,
drinking: undefined,
kids: undefined
}, // Пропускаем lifestyle, так как этих колонок нет
location: undefined, // Пропускаем location, так как этих колонок нет
searchPreferences: {
minAge: 18,
maxAge: 50,
@@ -466,9 +476,10 @@ export class ProfileService {
// Специальные случаи для некоторых полей
const specialCases: { [key: string]: string } = {
'interestedIn': 'looking_for',
'job': 'occupation',
'city': 'location',
// Удалили 'job': 'occupation', так как колонка occupation не существует
// Вместо этого используем job
'datingGoal': 'dating_goal'
// Удалили 'city': 'location', так как колонка location не существует
};
if (specialCases[str]) {
@@ -484,7 +495,7 @@ export class ProfileService {
await transaction(async (client) => {
// Удаляем связанные данные
await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]);
await client.query('DELETE FROM matches WHERE user1_id = $1 OR user2_id = $1', [userId]);
await client.query('DELETE FROM matches WHERE user_id_1 = $1 OR user_id_2 = $1', [userId]);
await client.query('DELETE FROM swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]);
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
});

View File

@@ -24,8 +24,9 @@ export class VipService {
// Проверить премиум статус пользователя
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
try {
// Проверяем существование пользователя
const result = await query(`
SELECT premium, premium_expires_at
SELECT id
FROM users
WHERE telegram_id = $1
`, [telegramId]);
@@ -34,27 +35,12 @@ export class VipService {
throw new BotError('User not found', 'USER_NOT_FOUND', 404);
}
const user = result.rows[0];
const isPremium = user.premium;
const expiresAt = user.premium_expires_at ? new Date(user.premium_expires_at) : undefined;
let daysLeft = undefined;
if (isPremium && expiresAt) {
const now = new Date();
const timeDiff = expiresAt.getTime() - now.getTime();
daysLeft = Math.ceil(timeDiff / (1000 * 3600 * 24));
// Если премиум истек
if (daysLeft <= 0) {
await this.removePremium(telegramId);
return { isPremium: false };
}
}
// Временно возвращаем false для всех пользователей, так как колонки premium нет
// В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно
return {
isPremium,
expiresAt,
daysLeft
isPremium: false,
expiresAt: undefined,
daysLeft: undefined
};
} catch (error) {
console.error('Error checking premium status:', error);
@@ -65,14 +51,9 @@ export class VipService {
// Добавить премиум статус
async addPremium(telegramId: string, durationDays: number = 30): Promise<void> {
try {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + durationDays);
await query(`
UPDATE users
SET premium = true, premium_expires_at = $2
WHERE telegram_id = $1
`, [telegramId, expiresAt]);
// Временно заглушка, так как колонок premium и premium_expires_at нет
console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`);
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
} catch (error) {
console.error('Error adding premium:', error);
throw error;
@@ -82,11 +63,9 @@ export class VipService {
// Удалить премиум статус
async removePremium(telegramId: string): Promise<void> {
try {
await query(`
UPDATE users
SET premium = false, premium_expires_at = NULL
WHERE telegram_id = $1
`, [telegramId]);
// Временно заглушка, так как колонок premium и premium_expires_at нет
console.log(`[VIP] Попытка удалить премиум для ${telegramId}`);
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
} catch (error) {
console.error('Error removing premium:', error);
throw error;