feat: новая функция
This commit is contained in:
275
backend/services/userContextService.js
Normal file
275
backend/services/userContextService.js
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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 (typeof userId === 'string' && userId.startsWith('guest_')) {
|
||||
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 (typeof userId === 'string' && userId.startsWith('guest_')) {
|
||||
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 {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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user