improvement for the backend api

This commit is contained in:
ivannoskov
2025-11-19 12:56:04 +03:00
parent b0a9934220
commit d4672ea32b
40 changed files with 2383 additions and 3313 deletions

191
lib/utils/file-parser.ts Normal file
View File

@@ -0,0 +1,191 @@
// ============================================================================
// File Parser Utilities
// ============================================================================
import * as XLSX from "xlsx";
import Papa from "papaparse";
export interface ParsedChannel {
telegram_id?: number;
title: string;
username?: string;
description?: string;
subscribers_count?: number;
row: number; // Номер строки для отчетов об ошибках
}
export interface ParseResult {
channels: ParsedChannel[];
errors: string[];
}
/**
* Парсит Excel файл (xlsx)
*/
export const parseExcelFile = async (file: File): Promise<ParseResult> => {
const errors: string[] = [];
const channels: ParsedChannel[] = [];
try {
const arrayBuffer = await file.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: "array" });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// Пропускаем заголовок (первая строка)
const rows = jsonData.slice(1) as any[][];
rows.forEach((row, index) => {
const rowNumber = index + 2; // +2 потому что пропустили заголовок и индекс с 0
// Пропускаем пустые строки
if (!row || row.length === 0 || !row[0]) {
return;
}
try {
const channel: ParsedChannel = {
row: rowNumber,
title: String(row[0] || "").trim(),
username: row[1] ? String(row[1]).trim().replace("@", "") : undefined,
telegram_id: row[2] ? parseInt(String(row[2])) : undefined,
subscribers_count: row[3] ? parseInt(String(row[3])) : undefined,
description: row[4] ? String(row[4]).trim() : undefined,
};
if (!channel.title) {
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
return;
}
channels.push(channel);
} catch (err) {
errors.push(
`Строка ${rowNumber}: ошибка парсинга - ${
err instanceof Error ? err.message : "неизвестная ошибка"
}`
);
}
});
} catch (err) {
errors.push(
`Ошибка чтения файла: ${
err instanceof Error ? err.message : "неизвестная ошибка"
}`
);
}
return { channels, errors };
};
/**
* Парсит CSV файл
*/
export const parseCsvFile = async (file: File): Promise<ParseResult> => {
return new Promise((resolve) => {
const errors: string[] = [];
const channels: ParsedChannel[] = [];
Papa.parse(file, {
header: false,
skipEmptyLines: true,
complete: (results) => {
const rows = results.data as string[][];
// Пропускаем заголовок
rows.slice(1).forEach((row, index) => {
const rowNumber = index + 2;
if (!row || row.length === 0 || !row[0]) {
return;
}
try {
const channel: ParsedChannel = {
row: rowNumber,
title: (row[0] || "").trim(),
username: row[1] ? row[1].trim().replace("@", "") : undefined,
telegram_id: row[2] ? parseInt(row[2]) : undefined,
subscribers_count: row[3] ? parseInt(row[3]) : undefined,
description: row[4] ? row[4].trim() : undefined,
};
if (!channel.title) {
errors.push(`Строка ${rowNumber}: отсутствует название канала`);
return;
}
channels.push(channel);
} catch (err) {
errors.push(
`Строка ${rowNumber}: ошибка парсинга - ${
err instanceof Error ? err.message : "неизвестная ошибка"
}`
);
}
});
resolve({ channels, errors });
},
error: (err) => {
errors.push(`Ошибка парсинга CSV: ${err.message}`);
resolve({ channels: [], errors });
},
});
});
};
/**
* Универсальная функция парсинга файла
*/
export const parseChannelsFile = async (file: File): Promise<ParseResult> => {
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension === "xlsx" || extension === "xls") {
return parseExcelFile(file);
} else if (extension === "csv") {
return parseCsvFile(file);
} else {
return {
channels: [],
errors: [
"Неподдерживаемый формат файла. Поддерживаются только .xlsx, .xls и .csv",
],
};
}
};
/**
* Скачать шаблон для импорта
*/
export const downloadTemplate = (format: "xlsx" | "csv") => {
const headers = [
"Название канала*",
"Username (без @)",
"Telegram ID",
"Подписчиков",
"Описание",
];
const example = [
"Пример канала",
"example_channel",
"-1001234567890",
"10000",
"Описание канала",
];
if (format === "xlsx") {
const ws = XLSX.utils.aoa_to_sheet([headers, example]);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Channels");
XLSX.writeFile(wb, "channels_template.xlsx");
} else {
const csv = Papa.unparse([headers, example]);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "channels_template.csv";
link.click();
}
};