/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/VC-HB3-Accelerator */ /** * User Context Service * Предоставляет информацию о пользователе: теги, имя, язык * Кэширование (TTL: 10 минут) */ const db = require('../db'); const encryptedDb = require('./encryptedDatabaseService'); const logger = require('../utils/logger'); const encryptionUtils = require('../utils/encryptionUtils'); // Кэш для пользовательских данных const userCache = new Map(); const CACHE_TTL = 10 * 60 * 1000; // 10 минут /** * Получить теги пользователя (tagIds) * @param {number} userId - ID пользователя * @returns {Promise>} Массив tagIds */ async function getUserTags(userId) { try { // Проверяем кэш const cacheKey = `tags_${userId}`; const cached = userCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) { return cached.data; } // Гостевые пользователи не имеют тегов if (isGuestId(userId)) { return []; } const query = db.getQuery(); const result = await query( 'SELECT tag_id FROM user_tag_links WHERE user_id = $1', [userId] ); const tagIds = result.rows.map(row => row.tag_id); // Сохраняем в кэш userCache.set(cacheKey, { data: tagIds, timestamp: Date.now() }); return tagIds; } catch (error) { logger.error('[UserContextService] Ошибка получения тегов пользователя:', error.message); return []; } } /** * Получить названия тегов по их ID * @param {Array} tagIds - Массив tagIds * @returns {Promise>} Массив названий тегов */ async function getTagNames(tagIds) { if (!tagIds || tagIds.length === 0) { return []; } try { // Проверяем кэш const cacheKey = `tagNames_${tagIds.sort().join(',')}`; const cached = userCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) { return cached.data; } // Находим таблицу "Теги клиентов" // encryptedDb.getData уже расшифровывает данные, поэтому используем поле name const tables = await encryptedDb.getData('user_tables', {}); const tagsTable = tables.find(table => table.name === 'Теги клиентов'); if (!tagsTable) { logger.warn('[UserContextService] Таблица "Теги клиентов" не найдена'); return []; } // Получаем строки таблицы (теги) const rows = await encryptedDb.getData('user_rows', { table_id: tagsTable.id, id: { $in: tagIds } }); // Получаем столбец с названием тега // encryptedDb.getData уже расшифровывает данные const columns = await encryptedDb.getData('user_columns', { table_id: tagsTable.id }); // Ищем столбец "Список тегов", затем "Название", затем первый текстовый столбец const nameColumn = columns.find(col => col.name === 'Список тегов' && col.type === 'text' ) || columns.find(col => (col.name === 'Название' || col.name === 'Название тега') && col.type === 'text' ) || columns.find(col => col.type === 'text'); if (!nameColumn) { logger.warn('[UserContextService] Столбец с названием тега не найден'); return []; } // Получаем значения ячеек const cellValues = await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(r => r.id) }, column_id: nameColumn.id }); // Создаем маппинг row_id -> название const tagNamesMap = new Map(); for (const cell of cellValues) { if (cell.value) { tagNamesMap.set(cell.row_id, cell.value); } } // Сортируем по порядку tagIds const tagNames = tagIds .map(tagId => tagNamesMap.get(tagId)) .filter(Boolean); // Сохраняем в кэш userCache.set(cacheKey, { data: tagNames, timestamp: Date.now() }); return tagNames; } catch (error) { logger.error('[UserContextService] Ошибка получения названий тегов:', error.message); return []; } } /** * Получить полный контекст пользователя * @param {number} userId - ID пользователя * @returns {Promise} Контекст пользователя */ async function getUserContext(userId) { try { // Проверяем кэш const cacheKey = `context_${userId}`; const cached = userCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) { return cached.data; } // Гостевые пользователи if (isGuestId(userId)) { return { id: userId, name: null, tags: [], tagNames: [], language: 'ru', role: 'guest' }; } // Получаем данные пользователя const query = db.getQuery(); const encryptionKey = encryptionUtils.getEncryptionKey(); const userResult = await query(` SELECT u.id, decrypt_text(u.first_name_encrypted, $1) as first_name, decrypt_text(u.last_name_encrypted, $1) as last_name, u.preferred_language, u.role FROM users u WHERE u.id = $2 `, [encryptionKey, userId]); if (userResult.rows.length === 0) { return null; } const user = userResult.rows[0]; // Формируем имя const firstName = user.first_name || ''; const lastName = user.last_name || ''; const name = [firstName, lastName].filter(Boolean).join(' ') || null; // Получаем теги const tagIds = await getUserTags(userId); const tagNames = await getTagNames(tagIds); const context = { id: userId, name, tags: tagIds, tagNames, language: user.preferred_language || 'ru', role: user.role || 'user' }; // Сохраняем в кэш userCache.set(cacheKey, { data: context, timestamp: Date.now() }); return context; } catch (error) { logger.error('[UserContextService] Ошибка получения контекста пользователя:', error.message); return null; } } /** * Проверяет, является ли идентификатор гостевым (строковым) * @param {unknown} userId * @returns {boolean} */ function isGuestId(userId) { if (typeof userId !== 'string') { return false; } const normalized = userId.trim(); return ( normalized.startsWith('guest_') || normalized.startsWith('web:guest_') || normalized.startsWith('telegram:guest_') || normalized.startsWith('email:guest_') || normalized.includes(':guest_') ); } /** * Инвалидация кэша для пользователя * @param {number} userId - ID пользователя */ function invalidateUserCache(userId) { const keysToDelete = []; for (const key of userCache.keys()) { if (key.includes(`_${userId}`) || key.includes(`_${userId}_`)) { keysToDelete.push(key); } } for (const key of keysToDelete) { userCache.delete(key); } logger.debug(`[UserContextService] Кэш инвалидирован для пользователя ${userId}`); } /** * Очистка всего кэша */ function clearCache() { userCache.clear(); logger.info('[UserContextService] Кэш очищен'); } /** * Получить статистику кэша */ function getCacheStats() { return { size: userCache.size, ttl: CACHE_TTL }; } module.exports = { getUserTags, getTagNames, getUserContext, invalidateUserCache, clearCache, getCacheStats, isGuestId };