Files
DLE/backend/services/userContextService.js
2025-11-12 22:34:39 +03:00

297 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* 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<Array<number>>} Массив 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<number>} tagIds - Массив tagIds
* @returns {Promise<Array<string>>} Массив названий тегов
*/
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<Object>} Контекст пользователя
*/
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
};