ваше сообщение коммита

This commit is contained in:
2025-08-01 12:33:18 +03:00
parent 33a10ea13a
commit 3ee29f16bd
11 changed files with 651 additions and 456 deletions

View File

@@ -86,6 +86,7 @@ const countriesRoutes = require('./routes/countries'); // Добавляем и
const russianClassifiersRoutes = require('./routes/russian-classifiers'); // Добавляем импорт российских классификаторов
const ollamaRoutes = require('./routes/ollama'); // Добавляем импорт Ollama маршрутов
const aiQueueRoutes = require('./routes/ai-queue'); // Добавляем импорт AI Queue маршрутов
const tagsRoutes = require('./routes/tags'); // Добавляем импорт маршрутов тегов
const app = express();
@@ -210,6 +211,7 @@ app.use('/api/countries', countriesRoutes); // Добавляем маршрут
app.use('/api/russian-classifiers', russianClassifiersRoutes); // Добавляем маршрут российских классификаторов
app.use('/api/ollama', ollamaRoutes); // Добавляем маршрут Ollama
app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI Queue
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
app.use('/api/messages', messagesRoutes);
app.use('/api/identities', identitiesRoutes);
app.use('/api/rag', ragRoutes); // Подключаем роут

View File

@@ -19,6 +19,18 @@ const { requireAuth } = require('../middleware/auth');
const vectorSearchClient = require('../services/vectorSearchClient');
const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHub');
// Вспомогательная функция для получения ключа шифрования
function getEncryptionKey() {
const fs = require('fs');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (!fs.existsSync(keyPath)) {
throw new Error('Encryption key file not found');
}
return fs.readFileSync(keyPath, 'utf8').trim();
}
router.use((req, res, next) => {
console.log('Tables router received:', req.method, req.originalUrl);
next();
@@ -28,17 +40,12 @@ router.use((req, res, next) => {
router.get('/', async (req, res, next) => {
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
const result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
@@ -54,17 +61,12 @@ router.post('/', async (req, res, next) => {
const { name, description, isRagSourceId } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
const result = await db.getQuery()(
@@ -81,17 +83,12 @@ router.post('/', async (req, res, next) => {
router.get('/rag-sources', async (req, res, next) => {
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
const result = await db.getQuery()(
@@ -111,17 +108,12 @@ router.get('/:id', async (req, res, next) => {
try {
const tableId = req.params.id;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
// Выполняем все 4 запроса параллельно для ускорения
@@ -164,13 +156,27 @@ function generatePlaceholder(name, existingPlaceholders = []) {
return '';
}).join('');
translit = translit.replace(/_+/g, '_').replace(/^_+|_+$/g, '');
// Если translit пустой, используем fallback
if (!translit) {
translit = 'column';
}
let base = translit;
let candidate = base;
let i = 1;
// Генерируем уникальный плейсхолдер
while (existingPlaceholders.includes(candidate)) {
candidate = `${base}_${i}`;
i++;
// Защита от бесконечного цикла
if (i > 1000) {
candidate = `${base}_${Date.now()}`;
break;
}
}
return candidate;
}
@@ -190,7 +196,13 @@ router.post('/:id/columns', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -201,8 +213,8 @@ router.post('/:id/columns', async (req, res, next) => {
console.error('Error reading encryption key:', keyError);
}
// Получаем уже существующие плейсхолдеры в таблице
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
// Получаем уже существующие плейсхолдеры во всей базе данных
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE placeholder IS NOT NULL', [])).rows;
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
const placeholder = generatePlaceholder(name, existingPlaceholders);
const result = await db.getQuery()(
@@ -228,7 +240,13 @@ router.post('/:id/rows', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -262,7 +280,13 @@ router.get('/:id/rows', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -347,7 +371,13 @@ router.post('/cell', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -365,21 +395,11 @@ router.post('/cell', async (req, res, next) => {
[row_id, column_id, value, encryptionKey]
);
// Получаем table_id и проверяем, является ли это таблицей тегов
// Получаем table_id
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
if (table) {
const tableId = table.table_id;
// Проверяем, является ли это таблицей "Теги клиентов"
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
console.log('🔄 [Tables] Проверяем таблицу:', { tableId, tableName: tableName?.name });
if (tableName && tableName.name === 'Теги клиентов') {
// Отправляем WebSocket уведомление об обновлении тегов
console.log('🔄 [Tables] Обновление ячейки в таблице тегов, отправляем уведомление');
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate();
}
// Получаем всю строку для upsert
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
if (rowData) {
@@ -408,35 +428,26 @@ router.delete('/row/:rowId', async (req, res, next) => {
// Получаем table_id
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
// Проверяем, является ли это таблицей тегов перед удалением
let isTagsTable = false;
if (table) {
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
if (!table) {
return res.status(404).json({ error: 'Row not found' });
}
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [table.table_id, encryptionKey])).rows[0];
isTagsTable = tableName && tableName.name === 'Теги клиентов';
}
const tableId = table.table_id;
// Удаляем строку
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
if (table) {
const tableId = table.table_id;
// Получаем все строки для rebuild
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -453,14 +464,6 @@ router.delete('/row/:rowId', async (req, res, next) => {
if (rebuildRows.length > 0) {
await vectorSearchClient.rebuild(tableId, rebuildRows);
}
}
// Отправляем WebSocket уведомление, если это была таблица тегов
if (isTagsTable) {
console.log('🔄 [Tables] Обновление строки в таблице тегов, отправляем уведомление');
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate();
}
// Отправляем WebSocket уведомление об обновлении таблицы
const { broadcastTableUpdate } = require('../wsHub');
@@ -468,6 +471,7 @@ router.delete('/row/:rowId', async (req, res, next) => {
res.json({ success: true });
} catch (err) {
console.error('[DELETE /row/:rowId] Error:', err);
next(err);
}
});
@@ -476,9 +480,29 @@ router.delete('/row/:rowId', async (req, res, next) => {
router.delete('/column/:columnId', async (req, res, next) => {
try {
const columnId = req.params.columnId;
// Получаем информацию о столбце перед удалением
const columnInfo = (await db.getQuery()('SELECT table_id FROM user_columns WHERE id = $1', [columnId])).rows[0];
if (!columnInfo) {
return res.status(404).json({ error: 'Column not found' });
}
// Удаляем все связанные данные в правильном порядке
// 1. Удаляем relations, связанные с этим столбцом
await db.getQuery()('DELETE FROM user_table_relations WHERE column_id = $1', [columnId]);
// 2. Удаляем все значения ячеек для этого столбца
await db.getQuery()('DELETE FROM user_cell_values WHERE column_id = $1', [columnId]);
// 3. Удаляем сам столбец
await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]);
// Отправляем WebSocket уведомление об обновлении таблицы
broadcastTableUpdate(columnInfo.table_id);
res.json({ success: true });
} catch (err) {
console.error('[DELETE /column/:columnId] Error:', err);
next(err);
}
});
@@ -492,7 +516,13 @@ router.patch('/column/:columnId', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -616,7 +646,13 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
@@ -665,37 +701,38 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем)
router.delete('/:id', requireAuth, async (req, res, next) => {
const dbModule = require('../db');
try {
// Логируем строку подключения и pool.options
console.log('[DIAG][DELETE] pool.options:', dbModule.pool.options);
console.log('[DIAG][DELETE] process.env.DATABASE_URL:', process.env.DATABASE_URL);
console.log('[DIAG][DELETE] process.env.DB_HOST:', process.env.DB_HOST);
console.log('[DIAG][DELETE] process.env.DB_NAME:', process.env.DB_NAME);
console.log('=== [DIAG] Попытка удаления таблицы ===');
console.log('Сессия пользователя:', req.session);
if (!req.session.isAdmin) {
console.log('[DIAG] Нет прав администратора');
return res.status(403).json({ error: 'Удаление доступно только администраторам' });
}
const tableId = Number(req.params.id);
console.log('[DIAG] id из запроса:', req.params.id, 'Преобразованный id:', tableId, 'typeof:', typeof tableId);
// Проверяем наличие таблицы перед удалением
const checkBefore = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
console.log('[DIAG] Таблица перед удалением:', checkBefore.rows);
const tableExists = (await db.getQuery()('SELECT id FROM user_tables WHERE id = $1', [tableId])).rows[0];
if (!tableExists) {
return res.status(404).json({ error: 'Table not found' });
}
// Пытаемся удалить
// Удаляем все связанные данные в правильном порядке
// 1. Удаляем все relations, где эта таблица является источником или целью
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id IN (SELECT id FROM user_rows WHERE table_id = $1) OR to_table_id = $1', [tableId]);
// 2. Удаляем все значения ячеек для строк этой таблицы
await db.getQuery()('DELETE FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId]);
// 3. Удаляем все строки таблицы
await db.getQuery()('DELETE FROM user_rows WHERE table_id = $1', [tableId]);
// 4. Удаляем все столбцы таблицы
await db.getQuery()('DELETE FROM user_columns WHERE table_id = $1', [tableId]);
// 5. Удаляем саму таблицу
const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1 RETURNING *', [tableId]);
console.log('[DIAG] Результат удаления (rowCount):', result.rowCount, 'rows:', result.rows);
// Проверяем наличие таблицы после удаления
const checkAfter = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
console.log('[DIAG] Таблица после удаления:', checkAfter.rows);
res.json({ success: true, deleted: result.rowCount });
} catch (err) {
console.error('[DIAG] Ошибка при удалении таблицы:', err);
console.error('[DELETE /:id] Error:', err);
next(err);
}
});
@@ -718,14 +755,51 @@ router.get('/:tableId/row/:rowId/relations', async (req, res, next) => {
router.post('/:tableId/row/:rowId/relations', async (req, res, next) => {
try {
const { tableId, rowId } = req.params;
const { column_id, to_table_id, to_row_id } = req.body;
const { column_id, to_table_id, to_row_ids } = req.body;
// Если передается массив to_row_ids - это массовое обновление
if (Array.isArray(to_row_ids)) {
// Удаляем старые связи для этого столбца
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
// Добавляем новые связи
if (to_row_ids.length > 0) {
const values = to_row_ids.map((to_row_id, index) =>
`($1, $2, $3, $${index + 4})`
).join(', ');
const params = [rowId, column_id, to_table_id, ...to_row_ids];
const result = await db.getQuery()(
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
VALUES ${values} RETURNING *`,
params
);
// Отправляем WebSocket уведомление
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate(null, rowId);
res.json(result.rows);
} else {
res.json([]);
}
} else {
// Одиночная связь
const { to_row_id } = req.body;
const result = await db.getQuery()(
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
VALUES ($1, $2, $3, $4) RETURNING *`,
[rowId, column_id, to_table_id, to_row_id]
);
// Отправляем WebSocket уведомление
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate(null, rowId);
res.json(result.rows[0]);
}
} catch (err) {
console.error('[POST /:tableId/row/:rowId/relations] Error:', err);
next(err);
}
});
@@ -742,55 +816,8 @@ router.delete('/:tableId/row/:rowId/relations/:relationId', async (req, res, nex
});
// --- Массовое обновление связей для multiselect-relation ---
router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
try {
const { tableId, rowId } = req.params;
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
console.log('🔄 [Tables] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
console.log('🔄 [Tables] Multirelations: обновление тегов, отправляем уведомление');
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate();
}
// Удаляем старые связи для этой строки/столбца
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
// Добавляем новые связи
for (const to_row_id of to_row_ids) {
await db.getQuery()(
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
VALUES ($1, $2, $3, $4)`,
[rowId, column_id, to_table_id, to_row_id]
);
}
// Отправляем WebSocket уведомление об обновлении связей
broadcastTableRelationsUpdate(tableId, rowId);
res.json({ success: true });
} catch (err) {
next(err);
}
});
// ПРИМЕЧАНИЕ: Для работы с тегами используйте /api/tags/user/:rowId/multirelations
// Этот endpoint удален для избежания дублирования
// Получить плейсхолдеры для всех столбцов таблицы
router.get('/:id/placeholders', async (req, res, next) => {
@@ -799,7 +826,13 @@ router.get('/:id/placeholders', async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
let encryptionKey;
try {
encryptionKey = getEncryptionKey();
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
return res.status(500).json({ error: 'Database encryption error' });
}
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';

134
backend/routes/tags.js Normal file
View File

@@ -0,0 +1,134 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const db = require('../db');
const { requireAuth } = require('../middleware/auth');
const { broadcastTagsUpdate } = require('../wsHub');
console.log('[tags.js] ROUTER LOADED');
router.use((req, res, next) => {
console.log('[tags.js] ROUTER REQUEST:', req.method, req.originalUrl);
next();
});
// PATCH /api/tags/user/:userId — установить теги пользователю
router.patch('/user/:userId', async (req, res) => {
const userId = Number(req.params.userId);
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
if (!Array.isArray(tags)) {
return res.status(400).json({ error: 'tags должен быть массивом' });
}
try {
// Удаляем старые связи
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
// Добавляем новые связи
for (const tagId of tags) {
await db.getQuery()(
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[userId, tagId]
);
}
// Отправляем WebSocket уведомление об обновлении тегов
broadcastTagsUpdate(null, userId);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/tags/user/:userId — получить все теги пользователя
router.get('/user/:userId', async (req, res) => {
const userId = Number(req.params.userId);
try {
const result = await db.getQuery()(
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
[userId]
);
res.json({ tags: result.rows.map(r => r.tag_id) });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// DELETE /api/tags/user/:userId/tag/:tagId — удалить тег у пользователя
router.delete('/user/:userId/tag/:tagId', async (req, res) => {
const userId = Number(req.params.userId);
const tagId = Number(req.params.tagId);
try {
await db.getQuery()(
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
[userId, tagId]
);
// Отправляем WebSocket уведомление об обновлении тегов
broadcastTagsUpdate(null, userId);
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// POST /api/tags/user/:rowId/multirelations — массовое обновление тегов через multirelations
router.post('/user/:rowId/multirelations', async (req, res) => {
const rowId = Number(req.params.rowId);
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id тегов
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
console.log('🔄 [Tags] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
console.log('🔄 [Tags] Multirelations: обновление тегов для строки:', rowId);
// Удаляем старые связи для этой строки/столбца
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
// Добавляем новые связи
for (const to_row_id of to_row_ids) {
await db.getQuery()(
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
VALUES ($1, $2, $3, $4)`,
[rowId, column_id, to_table_id, to_row_id]
);
}
// Отправляем WebSocket уведомление об обновлении тегов
broadcastTagsUpdate(null, rowId);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Этот endpoint предназначен только для работы с тегами' });
}
});
module.exports = router;

View File

@@ -561,67 +561,11 @@ router.post('/import', requireAuth, async (req, res) => {
}
});
// --- Работа с тегами пользователя через user_tag_links ---
// PATCH /api/users/:id/tags — установить теги пользователю
router.patch('/:id/tags', async (req, res) => {
const userId = Number(req.params.id);
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
if (!Array.isArray(tags)) {
return res.status(400).json({ error: 'tags должен быть массивом' });
}
try {
// Удаляем старые связи
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
// Добавляем новые связи
for (const tagId of tags) {
await db.getQuery()(
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[userId, tagId]
);
}
// Отправляем WebSocket уведомление об обновлении тегов
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate();
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// GET /api/users/:id/tags — получить все теги пользователя
router.get('/:id/tags', async (req, res) => {
const userId = Number(req.params.id);
try {
const result = await db.getQuery()(
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
[userId]
);
res.json({ tags: result.rows.map(r => r.tag_id) });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// DELETE /api/users/:id/tags/:tagId — удалить тег у пользователя
router.delete('/:id/tags/:tagId', async (req, res) => {
const userId = Number(req.params.id);
const tagId = Number(req.params.tagId);
try {
await db.getQuery()(
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
[userId, tagId]
);
// Отправляем WebSocket уведомление об обновлении тегов
const { broadcastTagsUpdate } = require('../wsHub');
broadcastTagsUpdate();
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// --- Работа с тегами перенесена в /api/tags ---
// Используйте следующие endpoints:
// PATCH /api/tags/user/:id — установить теги пользователю
// GET /api/tags/user/:id — получить теги пользователя
// DELETE /api/tags/user/:id/tag/:tagId — удалить тег у пользователя
// POST /api/tags/user/:id/multirelations — массовое обновление тегов
module.exports = router;

View File

@@ -20,6 +20,10 @@ const wsClients = new Map(); // userId -> Set of WebSocket connections
const tagsChangeCache = new Map();
const TAGS_CACHE_TTL = 5000; // 5 секунд
// Дебаунс для broadcastTagsUpdate
let tagsUpdateTimeout = null;
const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' });
@@ -234,20 +238,32 @@ function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
}
}
function broadcastTagsUpdate(targetUserId = null) {
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов');
function broadcastTagsUpdate(targetUserId = null, rowId = null) {
// Дебаунс: отменяем предыдущий таймаут
if (tagsUpdateTimeout) {
clearTimeout(tagsUpdateTimeout);
}
// Устанавливаем новый таймаут
tagsUpdateTimeout = setTimeout(() => {
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов', rowId ? `для строки ${rowId}` : '');
const message = JSON.stringify({
type: 'tags-updated',
timestamp: Date.now()
timestamp: Date.now(),
rowId: rowId // Добавляем информацию о конкретной строке
});
let sentCount = 0;
// Отправляем всем подключенным клиентам
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
console.log('🔔 [WebSocket] Отправляем tags-updated клиенту');
client.send(message);
sentCount++;
}
});
console.log(`🔔 [WebSocket] Отправлено tags-updated ${sentCount} клиентам`);
}, TAGS_UPDATE_DEBOUNCE);
}
function getConnectedUsers() {

View File

@@ -526,7 +526,7 @@ async function loadMultiRelationOptions() {
// Дебаунсинг для loadMultiRelationValues
let loadMultiRelationValuesTimer = null;
const LOAD_DEBOUNCE_DELAY = 100; // 100ms
const LOAD_DEBOUNCE_DELAY = 50; // 50ms (уменьшено для ускорения)
async function loadMultiRelationValues() {
// Проверяем, не загружены ли уже данные
@@ -625,13 +625,20 @@ async function saveMultiRelation() {
to_row_ids: editMultiRelationValues.value
};
console.log('[saveMultiRelation] POST payload:', payload);
const response = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/multirelations`, {
console.log('[TableCell] Отправляем запрос на обновление relations для строки:', props.rowId);
console.log('[TableCell] Данные запроса:', payload);
const response = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/relations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json().catch(() => ({}));
console.log('[saveMultiRelation] API response status:', response.status, 'result:', result);
console.log('[TableCell] Ответ сервера для строки:', props.rowId, 'статус:', response.status, 'результат:', result);
if (response.ok) {
console.log('[TableCell] Успешно сохранены теги для строки:', props.rowId);
} else {
console.error('[TableCell] Ошибка сохранения тегов для строки:', props.rowId, 'статус:', response.status);
}
editing.value = false;
await loadMultiRelationValues();
console.log('[saveMultiRelation] emitting update with:', editMultiRelationValues.value);
@@ -682,6 +689,9 @@ async function addTag() {
]);
console.log('[addTag] Тег добавлен в выбранные:', editMultiRelationValues.value);
// Сохраняем изменения, чтобы отправить WebSocket уведомление
await saveMultiRelation();
} catch (e) {
console.error('[addTag] Ошибка при добавлении тега:', e);
}
@@ -707,6 +717,9 @@ async function deleteTag(tagId) {
await loadMultiRelationOptions();
console.log('[deleteTag] Тег удален:', tagId);
// Сохраняем изменения, чтобы отправить WebSocket уведомление
await saveMultiRelation();
} catch (e) {
console.error('[deleteTag] Ошибка при удалении тега:', e);
}

View File

@@ -90,13 +90,19 @@
:resizable="false"
>
<template #header>
<button class="add-col-btn" @click="addColumn" title="Добавить столбец">
<button class="add-col-btn" @click.stop="openAddMenu($event)" title="Добавить">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="10" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="10" y="5.5" width="2" height="11" rx="1" fill="#4f8cff"/>
<rect x="5.5" y="10" width="11" height="2" rx="1" fill="#4f8cff"/>
</svg>
</button>
<teleport to="body">
<div v-if="showAddMenu" class="context-menu" :style="addMenuStyle">
<button class="menu-item" @click="addColumn">Добавить столбец</button>
<button class="menu-item" @click="addRow">Добавить строку</button>
</div>
</teleport>
</template>
<template #default="{ row }">
<button class="row-menu" @click.stop="openRowMenu(row, $event)"></button>
@@ -118,7 +124,7 @@
<!-- <button class="menu-item" @click="addColumn">Добавить столбец</button> -->
</div>
</teleport>
<div v-if="openedColMenuId || openedRowMenuId" class="menu-overlay" @click="closeMenus"></div>
<div v-if="openedColMenuId || openedRowMenuId || showAddMenu" class="menu-overlay" @click="closeMenus"></div>
<!-- Модалка добавления столбца -->
<div v-if="showAddColModal" class="modal-backdrop">
<div class="modal add-col-modal">
@@ -169,7 +175,9 @@ import axios from 'axios';
import { ElSelect, ElOption, ElButton } from 'element-plus';
import websocketService from '../../services/websocketService';
import cacheService from '../../services/cacheService';
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
let unsubscribeFromTableUpdate = null;
let unsubscribeFromTagsUpdate = null;
const { isAdmin } = useAuthContext();
const rebuilding = ref(false);
@@ -269,6 +277,10 @@ const openedRowMenuId = ref(null);
const colMenuStyle = ref('');
const rowMenuStyle = ref('');
// Меню добавления
const showAddMenu = ref(false);
const addMenuStyle = ref('');
function closeAddColModal() {
showAddColModal.value = false;
newColName.value = '';
@@ -546,12 +558,77 @@ onMounted(() => {
cacheService.clearTableCache(props.tableId);
fetchTable();
});
// Подписка на WebSocket обновления тегов
const { onTagsUpdate } = useTagsWebSocket();
console.log('[UserTableView] Подписываемся на обновления тегов для таблицы:', props.tableId);
console.log('[UserTableView] onTagsUpdate функция:', typeof onTagsUpdate);
unsubscribeFromTagsUpdate = onTagsUpdate(async (data) => {
console.log('[UserTableView] 🔔 ПОЛУЧЕНО СОБЫТИЕ TAGS-UPDATED!');
console.log('[UserTableView] Получено событие tags-updated, обновляем данные для таблицы:', props.tableId, data);
// Если есть информация о конкретной строке, обновляем только её
if (data && data.rowId) {
console.log('[UserTableView] Точечное обновление для строки:', data.rowId);
try {
// Очищаем кэш relations только для конкретной строки
const tagColumns = columns.value.filter(col =>
col.type === 'multirelation' &&
col.options?.relatedTableId
);
for (const col of tagColumns) {
cacheService.clearRelationsData(data.rowId, col.id);
}
console.log('[UserTableView] Кэш relations очищен для строки, обновляем данные строки:', data.rowId);
// Обновляем только данные конкретной строки
await updateRowData(data.rowId);
console.log('[UserTableView] Данные строки обновлены:', data.rowId);
} catch (error) {
console.error('[UserTableView] Ошибка при точечном обновлении:', error);
// Fallback: полная перезагрузка при ошибке
await fetchTable();
}
} else {
// Если нет информации о строке, используем старую логику
console.log('[UserTableView] Общее обновление тегов');
try {
// Очищаем кэш relations для всех строк этой таблицы
const tableRows = rows.value || [];
for (const row of tableRows) {
// Находим колонки с мульти-связями (теги)
const tagColumns = columns.value.filter(col =>
col.type === 'multirelation' &&
col.options?.relatedTableId
);
for (const col of tagColumns) {
cacheService.clearRelationsData(row.id, col.id);
}
}
console.log('[UserTableView] Кэш relations очищен, перезагружаем данные таблицы:', props.tableId);
await fetchTable();
console.log('[UserTableView] Данные таблицы перезагружены:', props.tableId);
} catch (error) {
console.error('[UserTableView] Ошибка при обновлении после tags-updated:', error);
// Fallback: полная перезагрузка при ошибке
cacheService.clearTableCache(props.tableId);
await fetchTable();
}
}
});
});
onUnmounted(() => {
if (unsubscribeFromTableUpdate) {
unsubscribeFromTableUpdate();
}
if (unsubscribeFromTagsUpdate) {
unsubscribeFromTagsUpdate();
}
});
// Для редактирования ячеек
@@ -619,6 +696,14 @@ function openRowMenu(row, event) {
function closeMenus() {
openedColMenuId.value = null;
openedRowMenuId.value = null;
showAddMenu.value = false;
}
function openAddMenu(event) {
showAddMenu.value = true;
openedColMenuId.value = null;
openedRowMenuId.value = null;
setMenuPosition(event, addMenuStyle);
}
function setMenuPosition(event, styleRef) {
// Позиционируем меню под кнопкой
@@ -676,6 +761,54 @@ async function rebuildIndex() {
}
}
// Функция для точечного обновления данных конкретной строки
async function updateRowData(rowId) {
const startTime = Date.now();
console.log(`[UserTableView] 🔄 Начало обновления данных строки ${rowId}`);
try {
// Находим строку в текущих данных
const rowIndex = rows.value.findIndex(row => row.id === rowId);
if (rowIndex === -1) {
console.log(`[UserTableView] Строка ${rowId} не найдена в текущих данных`);
return;
}
// Загружаем relations только для этой строки
const tagColumns = columns.value.filter(col =>
col.type === 'multirelation' &&
col.options?.relatedTableId
);
if (tagColumns.length > 0) {
console.log(`[UserTableView] 🔄 Загружаем relations для строки ${rowId} (${tagColumns.length} столбцов)`);
const relationPromises = tagColumns.map(col =>
fetch(`/api/tables/${col.table_id}/row/${rowId}/relations`)
.then(res => res.json())
.then(relations => {
// Сохраняем в кэш
cacheService.setRelationsData(rowId, col.id, relations);
return { rowId, colId: col.id, relations };
})
.catch(error => {
console.error(`[UserTableView] Ошибка загрузки relations для row:${rowId} col:${col.id}:`, error);
return { rowId, colId: col.id, relations: [] };
})
);
await Promise.all(relationPromises);
console.log(`[UserTableView] ✅ Relations для строки ${rowId} обновлены`);
}
const endTime = Date.now();
console.log(`[UserTableView] ✅ Завершено обновление строки ${rowId} за ${endTime - startTime}ms`);
} catch (error) {
console.error(`[UserTableView] ❌ Ошибка при обновлении строки ${rowId}:`, error);
throw error;
}
}
</script>
<style scoped>

View File

@@ -10,206 +10,40 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
import { ref, onMounted, onUnmounted } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import websocketServiceModule from '@/services/websocketService.js';
const { websocketService, onTableUpdate } = websocketServiceModule;
export function useTablesWebSocket() {
const ws = ref(null);
const isConnected = ref(false);
const isConnecting = ref(false); // Добавляем флаг для предотвращения множественных подключений
const tableUpdateCallbacks = ref(new Map()); // tableId -> callback
const tableRelationsUpdateCallbacks = ref(new Map()); // `${tableId}-${rowId}` -> callback
const pingInterval = ref(null); // Интервал для ping сообщений
function connect() {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
console.log('[TablesWebSocket] Уже подключены, пропускаем');
return; // Уже подключены
}
if (isConnecting.value) {
console.log('[TablesWebSocket] Уже пытаемся подключиться, пропускаем');
return; // Уже пытаемся подключиться
}
isConnecting.value = true;
// Определяем правильный URL для WebSocket
let wsUrl;
if (import.meta.env.DEV) {
// В режиме разработки используем прокси через Vite
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
} else {
// В продакшене используем тот же хост
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
}
console.log('[TablesWebSocket] Подключение к:', wsUrl);
console.log('[TablesWebSocket] Текущий хост:', window.location.host);
console.log('[TablesWebSocket] Протокол:', window.location.protocol);
try {
ws.value = new WebSocket(wsUrl);
} catch (error) {
console.error('[TablesWebSocket] Ошибка создания WebSocket:', error);
isConnecting.value = false;
return;
}
ws.value.onopen = () => {
console.log('[TablesWebSocket] Соединение установлено');
isConnected.value = true;
isConnecting.value = false;
// Запускаем ping каждые 30 секунд
pingInterval.value = setInterval(() => {
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
try {
ws.value.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
} catch (error) {
console.error('[TablesWebSocket] Ошибка отправки ping:', error);
}
}
}, 30000);
};
ws.value.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('[TablesWebSocket] Получено сообщение:', data);
// Обрабатываем pong ответ
if (data.type === 'pong') {
console.log('[TablesWebSocket] Получен pong ответ');
return;
}
if (data.type === 'table-updated') {
const callbacks = tableUpdateCallbacks.value.get(data.tableId);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
if (data.type === 'table-relations-updated') {
const key = `${data.tableId}-${data.rowId}`;
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
} catch (error) {
console.error('[TablesWebSocket] Ошибка обработки сообщения:', error);
}
};
ws.value.onclose = (event) => {
console.log('[TablesWebSocket] Соединение закрыто', {
code: event.code,
reason: event.reason,
wasClean: event.wasClean
});
isConnected.value = false;
isConnecting.value = false;
// Останавливаем ping интервал
if (pingInterval.value) {
clearInterval(pingInterval.value);
pingInterval.value = null;
}
// Переподключение только если это не было намеренное закрытие
if (event.code !== 1000) {
setTimeout(() => {
if (!isConnected.value && !isConnecting.value) {
console.log('[TablesWebSocket] Попытка переподключения...');
connect();
}
}, 3000);
}
};
ws.value.onerror = (error) => {
console.error('[TablesWebSocket] Ошибка соединения:', error);
console.error('[TablesWebSocket] WebSocket readyState:', ws.value?.readyState);
isConnected.value = false;
isConnecting.value = false;
};
}
// Подписка на обновления таблиц
function subscribeToTableUpdates(tableId, callback) {
if (!tableUpdateCallbacks.value.has(tableId)) {
tableUpdateCallbacks.value.set(tableId, []);
}
tableUpdateCallbacks.value.get(tableId).push(callback);
// Возвращаем функцию для отписки
return () => {
const callbacks = tableUpdateCallbacks.value.get(tableId);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
tableUpdateCallbacks.value.delete(tableId);
}
}
};
return onTableUpdate(tableId, callback);
}
// Подписка на обновления связей (relations)
function subscribeToTableRelationsUpdates(tableId, rowId, callback) {
const key = `${tableId}-${rowId}`;
if (!tableRelationsUpdateCallbacks.value.has(key)) {
tableRelationsUpdateCallbacks.value.set(key, []);
}
tableRelationsUpdateCallbacks.value.get(key).push(callback);
// Возвращаем функцию для отписки
return () => {
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
tableRelationsUpdateCallbacks.value.delete(key);
}
// Используем глобальный обработчик и фильтруем по tableId/rowId
const handler = (data) => {
if (data.tableId === tableId && data.rowId === rowId) {
callback(data);
}
};
}
function disconnect() {
if (ws.value) {
// Останавливаем ping интервал
if (pingInterval.value) {
clearInterval(pingInterval.value);
pingInterval.value = null;
}
// Корректно закрываем соединение
if (ws.value.readyState === WebSocket.OPEN) {
ws.value.close(1000, 'Manual disconnect');
}
ws.value = null;
}
isConnected.value = false;
isConnecting.value = false;
websocketService.on('table-relations-updated', handler);
// Возвращаем функцию для отписки
return () => websocketService.off('table-relations-updated', handler);
}
onMounted(() => {
connect();
// Соединение управляется websocketService, ничего не делаем
});
onUnmounted(() => {
disconnect();
// Соединение управляется websocketService, ничего не делаем
});
return {
isConnected,
connect,
disconnect,
subscribeToTableUpdates,
subscribeToTableRelationsUpdates
subscribeToTableRelationsUpdates,
};
}

View File

@@ -16,14 +16,16 @@ import websocketServiceModule from '../services/websocketService';
const { websocketService } = websocketServiceModule;
export function useTagsWebSocket() {
console.log('🏷️ [useTagsWebSocket] Композабл создан');
const tagsUpdateCallbacks = ref([]);
let debounceTimer = null;
const DEBOUNCE_DELAY = 1000; // 1 секунда
const isSubscribed = ref(false);
function onTagsUpdate(callback) {
tagsUpdateCallbacks.value.push(callback);
console.log('🏷️ [useTagsWebSocket] Регистрация колбэка');
// Возвращаем функцию для отписки
// Проверяем, не зарегистрирован ли уже этот колбэк
if (tagsUpdateCallbacks.value.includes(callback)) {
console.log('🏷️ [useTagsWebSocket] Колбэк уже зарегистрирован, пропускаем');
return () => {
const index = tagsUpdateCallbacks.value.indexOf(callback);
if (index > -1) {
@@ -32,28 +34,60 @@ export function useTagsWebSocket() {
};
}
tagsUpdateCallbacks.value.push(callback);
console.log('🏷️ [useTagsWebSocket] Количество колбэков:', tagsUpdateCallbacks.value.length);
// Возвращаем функцию для отписки
return () => {
console.log('🏷️ [useTagsWebSocket] Отписка колбэка');
const index = tagsUpdateCallbacks.value.indexOf(callback);
if (index > -1) {
tagsUpdateCallbacks.value.splice(index, 1);
console.log('🏷️ [useTagsWebSocket] Колбэк удален, осталось:', tagsUpdateCallbacks.value.length);
}
};
}
function handleTagsUpdate(data) {
console.log('🏷️ [useTagsWebSocket] Получено уведомление об обновлении тегов:', data);
console.log('🏷️ [useTagsWebSocket] Количество активных колбэков:', tagsUpdateCallbacks.value.length);
// Вызываем все зарегистрированные колбэки
tagsUpdateCallbacks.value.forEach(callback => {
tagsUpdateCallbacks.value.forEach((callback, index) => {
try {
console.log('🏷️ [useTagsWebSocket] Выполняем колбэк #', index);
callback(data);
} catch (error) {
console.error('🏷️ [useTagsWebSocket] Ошибка в колбэке:', error);
console.error('🏷️ [useTagsWebSocket] Ошибка в колбэке #', index, ':', error);
}
});
}
onMounted(() => {
console.log('🏷️ [useTagsWebSocket] onMounted вызван');
// Проверяем, не подписаны ли уже
if (isSubscribed.value) {
console.log('🏷️ [useTagsWebSocket] Уже подписаны, пропускаем');
return;
}
console.log('🏷️ [useTagsWebSocket] Подписываемся на tags-updated');
websocketService.on('tags-updated', handleTagsUpdate);
isSubscribed.value = true;
console.log('🏷️ [useTagsWebSocket] Подписка завершена');
});
onUnmounted(() => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
console.log('🏷️ [useTagsWebSocket] onUnmounted вызван');
if (isSubscribed.value) {
websocketService.off('tags-updated', handleTagsUpdate);
isSubscribed.value = false;
console.log('🏷️ [useTagsWebSocket] Отписка завершена');
}
// Очищаем все колбэки
tagsUpdateCallbacks.value = [];
console.log('🏷️ [useTagsWebSocket] Колбэки очищены');
});
return {

View File

@@ -57,20 +57,20 @@ export default {
},
// --- Работа с тегами пользователя ---
async addTagsToContact(contactId, tagIds) {
// PATCH /users/:id/tags { tags: [...] }
const res = await api.patch(`/users/${contactId}/tags`, { tags: tagIds });
// PATCH /api/tags/user/:id { tags: [...] }
const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds });
return res.data;
},
async getContactTags(contactId) {
// GET /users/:id/tags
const res = await api.get(`/users/${contactId}/tags`);
},
async getContactTags(contactId) {
// GET /api/tags/user/:id
const res = await api.get(`/tags/user/${contactId}`);
return res.data.tags || [];
},
async removeTagFromContact(contactId, tagId) {
// DELETE /users/:id/tags/:tagId
const res = await api.delete(`/users/${contactId}/tags/${tagId}`);
},
async removeTagFromContact(contactId, tagId) {
// DELETE /api/tags/user/:id/tag/:tagId
const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`);
return res.data;
}
}
};
export async function getContacts() {

View File

@@ -16,6 +16,7 @@
class WebSocketService {
constructor() {
console.log('🔌 [WebSocket] Конструктор вызван');
this.ws = null;
this.isConnected = false;
this.reconnectAttempts = 0;
@@ -23,10 +24,12 @@ class WebSocketService {
this.reconnectDelay = 1000; // 1 секунда
this.listeners = new Map();
this.userId = null;
console.log('🔌 [WebSocket] Конструктор завершен');
}
// Подключение к WebSocket серверу
connect(userId = null) {
console.log('🔌 [WebSocket] Попытка подключения, userId:', userId);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.log('🔌 [WebSocket] Уже подключен');
return;
@@ -37,11 +40,11 @@ class WebSocketService {
try {
// Определяем WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// В Docker окружении backend работает на порту 8000
const backendHost = window.location.hostname + ':8000';
const wsUrl = `${protocol}//${backendHost}/ws`;
// В Docker окружении используем тот же хост, что и для HTTP
const wsUrl = `${protocol}//${window.location.host}/ws`;
console.log('🔌 [WebSocket] Подключение к:', wsUrl);
console.log('🔌 [WebSocket] Текущий хост:', window.location.host);
this.ws = new WebSocket(wsUrl);
@@ -61,10 +64,35 @@ class WebSocketService {
this.emit('connected');
};
this.ws.onclose = (event) => {
console.log('🔌 [WebSocket] Соединение закрыто:', event.code, event.reason);
this.isConnected = false;
this.emit('disconnected', event);
// Попытка переподключения
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`🔄 [WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
setTimeout(() => {
this.connect(this.userId);
}, this.reconnectDelay * this.reconnectAttempts);
} else {
console.error('❌ [WebSocket] Превышено максимальное количество попыток переподключения');
this.emit('reconnect-failed');
}
};
this.ws.onerror = (error) => {
console.error('❌ [WebSocket] Ошибка соединения:', error);
this.emit('error', error);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('📨 [WebSocket] Получено сообщение:', data);
console.log('📨 [WebSocket] Тип сообщения:', data.type);
this.handleMessage(data);
} catch (error) {
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
@@ -139,8 +167,9 @@ class WebSocketService {
break;
case 'tags-updated':
console.log('🔔 [websocketService] Получено сообщение tags-updated');
this.emit('tags-updated');
console.log('🔔 [websocketService] Получено сообщение tags-updated:', data);
console.log('🔔 [websocketService] Количество слушателей tags-updated:', this.listeners.get('tags-updated')?.length || 0);
this.emit('tags-updated', data);
break;
case 'table-updated':
@@ -158,10 +187,12 @@ class WebSocketService {
// Подписка на события
on(event, callback) {
console.log('🔌 [WebSocket] Подписка на событие:', event);
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
console.log('🔌 [WebSocket] Количество слушателей для', event, ':', this.listeners.get(event).length);
}
// Отписка от событий
@@ -177,14 +208,20 @@ class WebSocketService {
// Эмиссия событий
emit(event, data) {
console.log('🔌 [WebSocket] Эмиссия события:', event, 'с данными:', data);
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => {
const callbacks = this.listeners.get(event);
console.log('🔌 [WebSocket] Количество колбэков для', event, ':', callbacks.length);
callbacks.forEach((callback, index) => {
try {
console.log('🔌 [WebSocket] Выполняем колбэк #', index, 'для события', event);
callback(data);
} catch (error) {
console.error(`❌ [WebSocket] Ошибка в обработчике события ${event}:`, error);
}
});
} else {
console.log('🔌 [WebSocket] Нет слушателей для события:', event);
}
}
@@ -212,6 +249,7 @@ class WebSocketService {
// Создаем единственный экземпляр
const websocketService = new WebSocketService();
console.log('🔌 [WebSocket] Сервис создан');
// Подписчики на обновления таблиц: tableId -> [callback]
const tableUpdateSubscribers = {};
@@ -231,3 +269,17 @@ export default {
websocketService,
onTableUpdate,
};
console.log('🔌 [WebSocket] Экспорт завершен');
// Автоматически подключаемся при загрузке модуля
console.log('🔌 [WebSocket] Автоматическое подключение...');
setTimeout(() => {
console.log('🔌 [WebSocket] Подключаемся через 1 секунду...');
websocketService.connect();
}, 1000);
// Добавляем периодическую проверку состояния соединения
setInterval(() => {
const status = websocketService.getStatus();
console.log('🔌 [WebSocket] Статус соединения:', status);
}, 10000); // Проверяем каждые 10 секунд