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

This commit is contained in:
2025-05-06 21:34:47 +03:00
parent e3b13bb175
commit 78b59a17de
13 changed files with 12892 additions and 5205 deletions

101
backend/routes/geocoding.js Normal file
View File

@@ -0,0 +1,101 @@
// backend/routes/geocoding.js
const express = require('express');
const router = express.Router();
const axios = require('axios'); // Убедитесь, что axios установлен на бэкенде
const logger = require('../utils/logger'); // Если используете логгер
/**
* @swagger
* tags:
* name: Geocoding
* description: Прокси для сервисов геокодирования (например, Nominatim)
*/
/**
* @swagger
* /api/geocoding/nominatim-search:
* get:
* summary: Проксирует запрос к Nominatim API (search)
* tags: [Geocoding]
* description: Перенаправляет GET запрос со всеми его query параметрами к https://nominatim.openstreetmap.org/search. Это необходимо для обхода CORS ограничений в браузере.
* parameters:
* - in: query
* name: q
* schema:
* type: string
* required: true
* description: Строка адреса для поиска.
* - in: query
* name: format
* schema:
* type: string
* required: true
* description: Формат ответа (например, jsonv2).
* - in: query
* name: addressdetails
* schema:
* type: integer
* enum: [0, 1]
* description: Включить детализированный адрес (0 или 1).
* - in: query
* name: limit
* schema:
* type: integer
* description: Максимальное количество возвращаемых результатов.
* # Можно добавить сюда и другие параметры Nominatim API по мере необходимости
* responses:
* 200:
* description: Успешный ответ от Nominatim.
* content:
* application/json:
* schema:
* type: object # Или array, в зависимости от ответа Nominatim
* 500:
* description: Ошибка при запросе к Nominatim или внутренняя ошибка сервера.
*/
router.get('/nominatim-search', async (req, res) => {
try {
// Формируем URL для Nominatim, используя все query параметры из исходного запроса
const queryParams = new URLSearchParams(req.query);
const nominatimUrl = `https://nominatim.openstreetmap.org/search?${queryParams.toString()}`;
if (logger && typeof logger.info === 'function') {
logger.info(`[Geocoding] Proxying request to Nominatim: ${nominatimUrl}`);
} else {
console.log(`[Geocoding] Proxying request to Nominatim: ${nominatimUrl}`);
}
const nominatimResponse = await axios.get(nominatimUrl);
res.json(nominatimResponse.data);
} catch (error) {
let errorMessage = error.message;
let errorStatus = 500;
let errorDetails = null;
if (error.response) {
// Ошибка пришла от Nominatim (или сети)
errorMessage = error.response.data?.message || error.response.statusText || 'Error fetching data from Nominatim';
errorStatus = error.response.status || 500;
errorDetails = error.response.data;
} else if (error.request) {
// Запрос был сделан, но ответ не получен
errorMessage = 'No response received from Nominatim';
}
// Иначе это ошибка настройки axios или другая внутренняя ошибка
if (logger && typeof logger.error === 'function') {
logger.error(`[Geocoding] Error proxying to Nominatim: ${errorMessage}`, { status: errorStatus, details: errorDetails, query: req.query });
} else {
console.error(`[Geocoding] Error proxying to Nominatim: ${errorMessage}`, { status: errorStatus, details: errorDetails, query: req.query });
}
res.status(errorStatus).json({
message: 'Error processing geocoding request',
details: errorDetails || errorMessage
});
}
});
module.exports = router;

297
backend/routes/isic.js Normal file
View File

@@ -0,0 +1,297 @@
const express = require('express');
const router = express.Router();
const { pool } = require('../db'); // Убедитесь, что путь к вашему db-коннектору правильный
const logger = require('../utils/logger'); // Если используете логгер
/**
* @swagger
* tags:
* name: ISIC
* description: API для кодов Международной стандартной отраслевой классификации (ISIC)
*/
/**
* @swagger
* /api/isic/codes:
* get:
* summary: Получить список кодов ISIC с фильтрацией и пагинацией
* tags: [ISIC]
* parameters:
* - in: query
* name: level
* schema:
* type: integer
* description: Фильтр по уровню кода (1-4, иногда 5 или 6)
* - in: query
* name: parent_code
* schema:
* type: string
* description: Фильтр по родительскому коду (для получения дочерних кодов)
* - in: query
* name: search
* schema:
* type: string
* description: Поисковый запрос по коду или описанию
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Номер страницы для пагинации
* - in: query
* name: limit
* schema:
* type: integer
* default: 25
* description: Количество элементов на странице
* responses:
* 200:
* description: Список кодов ISIC
* content:
* application/json:
* schema:
* type: object
* properties:
* totalItems:
* type: integer
* codes:
* type: array
* items:
* $ref: '#/components/schemas/IsicCode'
* totalPages:
* type: integer
* currentPage:
* type: integer
* 404:
* description: Родительский код не найден
* 500:
* description: Ошибка сервера
*/
router.get('/codes', async (req, res) => {
const { level, parent_code, search } = req.query;
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 25;
const offset = (page - 1) * limit;
const baseQuerySelect = `
SELECT c.code, c.description, c.code_level, c.explanatory_note_inclusion, c.explanatory_note_exclusion,
l.level_name_en,
c.level1, c.level2, c.level3, c.level4, c.level5, c.level6
FROM isic_rev4_codes c
LEFT JOIN isic_rev4_level_names l ON c.code_level = l.code_level
`;
// Для подсчета JOIN с level_names не нужен, если только по level_name_en не будет фильтрации (пока нет)
const baseQueryCount = `
SELECT COUNT(*) AS total
FROM isic_rev4_codes c
`;
const conditions = [];
const queryParams = []; // Параметры для основного запроса (включая limit/offset)
const countQueryParams = []; // Параметры только для WHERE части (для count запроса)
if (level) {
const levelParam = parseInt(level, 10);
conditions.push(`c.code_level = $${queryParams.push(levelParam)}`);
countQueryParams.push(levelParam);
}
if (parent_code) {
try {
const parentResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]);
if (parentResult.rows.length > 0) {
const parentLevel = parentResult.rows[0].code_level;
if (parentLevel >= 1 && parentLevel < 6) {
conditions.push(`c.level${parentLevel} = $${queryParams.push(parent_code)}`);
countQueryParams.push(parent_code);
const childLevel = parentLevel + 1;
conditions.push(`c.code_level = $${queryParams.push(childLevel)}`);
countQueryParams.push(childLevel);
} else {
// Родительский код на максимальном уровне, нет дочерних.
// Возвращаем пустой результат сразу.
return res.json({ totalItems: 0, codes: [], totalPages: 0, currentPage: page });
}
} else {
// parent_code не найден
logger.warn(`Parent code not found: ${parent_code}`);
return res.status(404).json({ error: 'Parent code not found', totalItems: 0, codes: [], totalPages: 0, currentPage: page });
}
} catch (dbError) {
logger.error('Error fetching parent_code level:', dbError);
return res.status(500).json({ error: 'Internal server error while fetching parent data' });
}
}
if (search) {
const searchPattern = `%${search}%`;
// Используем один параметр для обоих ILIKE, чтобы $N был корректным в countQueryParams
conditions.push(`(c.code ILIKE $${queryParams.push(searchPattern)} OR c.description ILIKE $${queryParams.length})`); // $N для второго ILIKE будет тем же, что и для первого
countQueryParams.push(searchPattern);
}
let whereClause = '';
if (conditions.length > 0) {
// Переиндексируем плейсхолдеры для whereClause, т.к. queryParams и countQueryParams теперь разные
// Это более сложный момент, проще собирать whereClause с $1, $2 и т.д. и передавать countQueryParams
// Для простоты сейчас оставим как было, но это означает, что queryParams используется для генерации whereClause,
// а потом из него берутся countQueryParams.
// Корректный способ: перестроить whereClause для count с правильными индексами
let countWhereClauseConditions = [];
let currentCountParamIndex = 1;
if (level) {
countWhereClauseConditions.push(`c.code_level = $${currentCountParamIndex++}`);
}
if (parent_code) {
// Предполагаем, что parent_code уже добавлен в countQueryParams
const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Нужно будет передать parent_code в countQueryParams
if (parentLevelResult.rows.length > 0) {
const parentLevel = parentLevelResult.rows[0].code_level;
if (parentLevel >=1 && parentLevel < 6) {
countWhereClauseConditions.push(`c.level${parentLevel} = $${currentCountParamIndex++}`);
countWhereClauseConditions.push(`c.code_level = $${currentCountParamIndex++}`);
}
}
}
if (search) {
countWhereClauseConditions.push(`(c.code ILIKE $${currentCountParamIndex} OR c.description ILIKE $${currentCountParamIndex})`);
currentCountParamIndex++;
}
whereClause = countWhereClauseConditions.length > 0 ? ' WHERE ' + countWhereClauseConditions.join(' AND ') : '';
}
// Запрос для получения данных с пагинацией
const dataQueryPlaceholders = queryParams.map((_, i) => `$${i + 1}`).join(', '); // Это неверно для WHERE
// Формируем finalQuery с плейсхолдерами, соответствующими queryParams
let finalQueryWhereClause = '';
if (conditions.length > 0) {
let currentQueryParamIndex = 1;
const queryWhereConditions = [];
if (level) queryWhereConditions.push(`c.code_level = $${currentQueryParamIndex++}`);
if (parent_code) {
const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Это дублирование, лучше получить parentLevel один раз
if (parentLevelResult.rows.length > 0) {
const parentLevel = parentLevelResult.rows[0].code_level;
if (parentLevel >=1 && parentLevel < 6) {
queryWhereConditions.push(`c.level${parentLevel} = $${currentQueryParamIndex++}`);
queryWhereConditions.push(`c.code_level = $${currentQueryParamIndex++}`);
}
}
}
if (search) queryWhereConditions.push(`(c.code ILIKE $${currentQueryParamIndex} OR c.description ILIKE $${currentQueryParamIndex})`); // searchPattern идет одним параметром
finalQueryWhereClause = queryWhereConditions.length > 0 ? ' WHERE ' + queryWhereConditions.join(' AND ') : '';
}
const finalQuery = `${baseQuerySelect} ${finalQueryWhereClause} ORDER BY c.sort_order, c.code LIMIT $${queryParams.push(limit)} OFFSET $${queryParams.push(offset)}`;
const finalCountQuery = `${baseQueryCount} ${whereClause}`;
try {
logger.debug('Executing count query:', finalCountQuery, 'Params:', countQueryParams);
const totalItemsResult = await pool.query(finalCountQuery, countQueryParams);
const totalItems = parseInt(totalItemsResult.rows[0].total, 10);
// Параметры для основного запроса - это все, что в queryParams (включая limit и offset)
logger.debug('Executing data query:', finalQuery, 'Params:', queryParams);
const result = await pool.query(finalQuery, queryParams);
res.json({
totalItems,
codes: result.rows,
totalPages: Math.ceil(totalItems / limit),
currentPage: page,
});
} catch (error) {
logger.error('Error fetching ISIC codes:', error);
if (error.query) logger.error('Failed Query:', error.query);
if (error.parameters) logger.error('Failed Params:', error.parameters);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* @swagger
* /api/isic/tree:
* get:
* summary: Получить иерархическое дерево кодов ISIC (или его часть)
* tags: [ISIC]
* parameters:
* - in: query
* name: root_code
* schema:
* type: string
* description: Код ISIC, с которого начинать построение дерева (например, 'A' или '01'). Если не указан, вернет все секции (уровень 1).
* - in: max_depth
* schema:
* type: integer
* default: 2
* description: Максимальная глубина дерева для загрузки.
* responses:
* 200:
* description: Дерево кодов ISIC
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/IsicTreeNode' # Определим эту схему позже
* 500:
* description: Ошибка сервера
*/
router.get('/tree', async (req, res) => {
const { root_code, max_depth = 2 } = req.query;
// Эта функция потребует рекурсивного запроса или сложного CTE (Common Table Expression) в SQL
// для построения дерева. Это более сложная задача.
// Для начала можно сделать упрощенную версию, которая возвращает один уровень вложенности.
try {
let items;
if (!root_code) { // Если нет root_code, возвращаем секции (уровень 1)
const result = await pool.query(
"SELECT code, description, code_level FROM isic_rev4_codes WHERE code_level = 1 ORDER BY sort_order, code"
);
items = result.rows.map(row => ({ ...row, children: [] })); // Добавляем пустой массив children
} else {
// Получаем сам root_code
const rootResult = await pool.query(
"SELECT code, description, code_level FROM isic_rev4_codes WHERE code = $1",
[root_code]
);
if (rootResult.rows.length === 0) {
return res.status(404).json({ error: 'Root code not found' });
}
const rootNode = { ...rootResult.rows[0], children: [] };
// Получаем прямых потомков (упрощенный пример для одного уровня вложенности)
let childrenQuery = '';
const childrenParams = [root_code];
const rootLevel = rootNode.code_level;
if (rootLevel === 1) childrenQuery = "SELECT code, description, code_level FROM isic_rev4_codes WHERE level1 = $1 AND code_level = 2 ORDER BY sort_order, code";
else if (rootLevel === 2) childrenQuery = "SELECT code, description, code_level FROM isic_rev4_codes WHERE level2 = $1 AND code_level = 3 ORDER BY sort_order, code";
else if (rootLevel === 3) childrenQuery = "SELECT code, description, code_level FROM isic_rev4_codes WHERE level3 = $1 AND code_level = 4 ORDER BY sort_order, code";
else if (rootLevel === 4) childrenQuery = "SELECT code, description, code_level FROM isic_rev4_codes WHERE level4 = $1 AND code_level = 5 ORDER BY sort_order, code";
else if (rootLevel === 5) childrenQuery = "SELECT code, description, code_level FROM isic_rev4_codes WHERE level5 = $1 AND code_level = 6 ORDER BY sort_order, code";
if (childrenQuery) {
const childrenResult = await pool.query(childrenQuery, childrenParams);
rootNode.children = childrenResult.rows.map(row => ({ ...row, children: [] }));
}
items = [rootNode];
}
res.json(items);
} catch (error)
{
logger.error('Error fetching ISIC tree:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;