sozday-polnostyu-rabochiy-in/server.js

232 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.

const http = require("http");
const fs = require("fs");
const path = require("path");
const XLSX = require("xlsx");
const formidable = require("formidable");
const PORT = process.env.PORT || 3000;
const DATA_DIR = path.join(__dirname, "uploads");
const ANALYSIS_FILE = path.join(__dirname, "analysis.json");
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR);
function readAnalysis() {
try { return JSON.parse(fs.readFileSync(ANALYSIS_FILE, "utf8")); } catch (_) { return null; }
}
function writeAnalysis(data) {
fs.writeFileSync(ANALYSIS_FILE, JSON.stringify(data, null, 2));
}
function body(req) {
return new Promise((resolve) => {
let s = "";
req.on("data", (c) => (s += c));
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
});
}
function parseFile(filepath) {
const ext = path.extname(filepath).toLowerCase();
if (ext === ".csv") {
const content = fs.readFileSync(filepath, "utf8");
const lines = content.split(/\r?\n/).filter(l => l.trim());
if (lines.length < 2) return { columns: [], rows: [] };
const parseLine = (line) => {
const result = [];
let current = '', inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') { inQuotes = !inQuotes; }
else if (char === ',' && !inQuotes) { result.push(current.trim()); current = ''; }
else { current += char; }
}
result.push(current.trim());
return result;
};
const columns = parseLine(lines[0]);
const rows = lines.slice(1).map(line => {
const values = parseLine(line);
const obj = {};
columns.forEach((col, i) => { obj[col] = values[i] || ''; });
return obj;
});
return { columns, rows };
} else {
const wb = XLSX.readFile(filepath);
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(sheet);
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
return { columns, rows };
}
}
function calculateStats(rows, columns) {
const stats = {};
const numericCols = columns.filter(col => {
const sample = rows.slice(0, 10).map(r => r[col]);
return sample.some(v => !isNaN(parseFloat(v)) && isFinite(v));
});
numericCols.forEach(col => {
const values = rows.map(r => parseFloat(r[col])).filter(v => !isNaN(v));
if (values.length > 0) {
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
const sorted = [...values].sort((a, b) => a - b);
const median = sorted.length % 2 === 0
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
: sorted[Math.floor(sorted.length / 2)];
stats[col] = { sum, avg, min, max, median, count: values.length };
}
});
return stats;
}
function generateInsights(rows, columns, stats) {
const insights = [];
const totalRows = rows.length;
if (totalRows > 0) {
insights.push({ type: "info", text: `Всего записей: ${totalRows}`, category: "Общие данные" });
}
Object.keys(stats).forEach(col => {
const s = stats[col];
if (s.count > totalRows * 0.5) {
insights.push({
type: "metric",
text: `Среднее по "${col}": ${s.avg.toFixed(2)}`,
category: "Статистика",
value: s.avg
});
if (s.max > s.avg * 2) {
insights.push({
type: "warning",
text: `Выявлен аномальный максимум в "${col}": ${s.max} (в ${((s.max/s.avg).toFixed(1))} раз выше среднего)`,
category: "Аномалии"
});
}
}
});
const stringCols = columns.filter(col => !stats[col]);
stringCols.forEach(col => {
const values = rows.map(r => r[col]).filter(v => v);
const unique = [...new Set(values)];
if (unique.length < totalRows * 0.3 && unique.length > 1) {
const counts = {};
values.forEach(v => { counts[v] = (counts[v] || 0) + 1; });
const top = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 3);
if (top.length > 0) {
insights.push({
type: "info",
text: `Наиболее частые значения в "${col}": ${top.map(([k, v]) => `${k} (${v})`).join(', ')}`,
category: "Категории"
});
}
}
});
if (insights.length === 0) {
insights.push({ type: "info", text: "Загрузите данные для получения аналитических выводов", category: "Рекомендации" });
}
return insights;
}
http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
res.writeHead(204);
return res.end();
}
if (req.method === "GET" && req.url === "/api/analysis") {
const analysis = readAnalysis();
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(analysis || { columns: [], rows: [], stats: {}, insights: [] }));
}
if (req.method === "POST" && req.url === "/api/upload") {
const form = formidable({ uploadDir: DATA_DIR, keepExtensions: true });
form.parse(req, async (err, fields, files) => {
if (err) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: err.message }));
}
const file = files.file && files.file[0];
if (!file) {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "Файл не загружен" }));
}
try {
const { columns, rows } = parseFile(file.filepath);
const stats = calculateStats(rows, columns);
const insights = generateInsights(rows, columns, stats);
const analysis = { columns, rows, stats, insights, filename: file.originalFilename };
writeAnalysis(analysis);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, columns, rows: rows.slice(0, 100), stats, insights }));
} catch (e) {
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
if (req.method === "GET" && req.url.startsWith("/api/data")) {
const analysis = readAnalysis();
if (!analysis) {
res.writeHead(404, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "Данные не загружены" }));
}
const url = new URL(req.url, "http://localhost");
const filterCol = url.searchParams.get("filterColumn");
const filterVal = url.searchParams.get("filterValue");
const sortCol = url.searchParams.get("sortColumn");
const sortOrder = url.searchParams.get("sortOrder") || "asc";
const limit = parseInt(url.searchParams.get("limit")) || 100;
let filtered = [...analysis.rows];
if (filterCol && filterVal) {
filtered = filtered.filter(r => String(r[filterCol] || "").toLowerCase().includes(filterVal.toLowerCase()));
}
if (sortCol) {
filtered.sort((a, b) => {
const aVal = parseFloat(a[sortCol]) || a[sortCol];
const bVal = parseFloat(b[sortCol]) || b[sortCol];
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
return 0;
});
}
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({
columns: analysis.columns,
rows: filtered.slice(0, limit),
total: filtered.length,
stats: analysis.stats,
insights: analysis.insights
}));
}
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
return res.end(fs.readFileSync(full));
}
res.writeHead(404);
res.end("Not found");
}).listen(PORT, () => console.log("Аналитическое приложение на порту " + PORT));