Files
labweb/routes/router.js
2025-12-16 11:39:15 +08:00

453 lines
19 KiB
JavaScript
Raw Permalink 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 express = require('express');
const router = express.Router();
const multer = require('multer');
const { nanoid } = require('nanoid');
const path = require('path');
const fs = require('fs');
// Import separated modules
const mediaModel = require('../utils/mediaModel');
const jobService = require('../utils/job');
const { pool } = require('../config/db');
// --- Multer Config ---
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const jobId = req.jobId;
// 使用项目根目录下的 results 文件夹,而不是 routes/results
const dir = path.join(__dirname, '..', 'results', jobId, 'uploads');
try {
fs.mkdirSync(dir, { recursive: true });
console.log(`[Upload] Created directory: ${dir}`);
console.log(`[Upload] Job ID: ${jobId}, File: ${file.originalname}`);
} catch (error) {
console.error(`[Upload] Failed to create directory ${dir}:`, error);
return cb(error);
}
cb(null, dir);
},
filename: (req, file, cb) => {
console.log(`[Upload] Saving file: ${file.originalname}`);
cb(null, file.originalname);
}
});
const upload = multer({ storage }).array('files', 30);
// --- Task API Routes ---
router.post('/upload', (req, res, next) => {
req.jobId = nanoid(12);
console.log(`[Upload] New upload request, Job ID: ${req.jobId}`);
next();
}, upload, async (req, res) => {
try {
console.log(`[Upload] Files received: ${req.files ? req.files.length : 0}`);
console.log(`[Upload] Analysis type: ${req.body?.analysis_type || 'nutrition'}`);
console.log(`[Upload] Email: ${req.body?.email || 'none'}`);
if (!req.files || req.files.length === 0) {
console.error('[Upload] No files received');
return res.status(400).json({ success: false, error: 'No files' });
}
const fileData = req.files.map(f => {
console.log(`[Upload] File saved: ${f.path}, Size: ${f.size} bytes`);
return {
filePath: f.path,
fileName: f.originalname,
fileSize: f.size,
mimeType: f.mimetype
};
});
console.log(`[Upload] Creating job with ID: ${req.jobId}`);
const job = jobService.createJob(req.jobId, req.body?.analysis_type || 'nutrition', req.body?.email, fileData);
console.log(`[Upload] Job created successfully: ${job.id}`);
res.json({ success: true, analysis_id: job.id, message: 'Job started' });
} catch (e) {
console.error('[Upload] Error:', e);
res.status(500).json({ success: false, error: e.message });
}
});
router.get('/status/:id', (req, res) => {
const job = jobService.getJob(req.params.id);
if (!job) return res.status(404).json({ success: false, error: 'Job not found' });
res.json({
success: true,
status: job.status,
progress: job.progress,
eta_seconds: job.eta_seconds,
result: job.status === 'completed' ? job.result : undefined,
error: job.status === 'failed' ? job.error : undefined
});
});
router.post('/stop/:id', async (req, res) => {
try {
await jobService.stopJob(req.params.id);
res.json({ success: true, message: 'Job stopped' });
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
router.get('/download/:id', (req, res) => {
const job = jobService.getJob(req.params.id);
if (!job || job.status !== 'completed' || !job.result) return res.status(400).json({ error: 'Not ready' });
res.download(job.result.result_file, `analysis_${req.params.id}.csv`);
});
router.post('/send-results-email', async (req, res) => {
try {
await jobService.resendEmail(req.body.analysis_id, req.body.email);
res.json({ success: true });
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
// --- Data & Browse API Routes ---
router.get('/browse', async (req, res) => {
try {
const { page = 1, pageSize = 20, chartData = false } = req.query;
const [data, total] = await Promise.all([
mediaModel.getBrowseData(req.query, page, pageSize),
mediaModel.getTotalCount(req.query)
]);
// console.log(data)
// console.log(total)
let sankeyData = null;
if (chartData === 'true') {
sankeyData = await mediaModel.getSankeyData(req.query);
}
// console.log(sankeyData)
res.json({
success: true,
data,
pagination: {
currentPage: parseInt(page),
pageSize: parseInt(pageSize),
total,
totalPages: Math.ceil(total / pageSize)
},
sankeyData
});
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
router.get('/microbial-detail', async (req, res) => {
try {
const data = await mediaModel.getMicrobialDetail(req.query.name, req.query.level);
if (!data) return res.json({ success: false, message: 'Not found' });
res.json({ success: true, data });
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
router.get('/taxonomy-stats', async (req, res) => {
try {
const stats = await mediaModel.getTaxonomyStats(req.query.level || 'phylum', req.query.cultured_type || 'cultured');
res.json({ success: true, ...stats });
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.get('/physchem-stats', async (req, res) => {
try {
const { type = 'o2', cultured_type = 'cultured' } = req.query;
// 验证 type 参数
const validTypes = ['o2', 'ph', 'temperature'];
if (!validTypes.includes(type)) {
return res.status(400).json({
success: false,
message: 'Invalid physical and chemical properties type',
validTypes: validTypes
});
}
const data = await mediaModel.getPhyschemStats(type, cultured_type);
res.json({ success: true, data, type: type });
} catch (e) {
console.error('Error in /physchem-stats:', e);
res.status(500).json({
success: false,
error: e.message,
message: 'Failed to get physical and chemical properties statistics'
});
}
});
router.get('/nutrition-stats', async (req, res) => {
try {
const stats = await mediaModel.getNutritionStats(req.query.cultured_type || 'cultured');
res.json({ success: true, ...stats });
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.get('/sunburst-stats', async (req, res) => {
try {
const data = await mediaModel.getSunburstData(req.query);
res.json({ success: true, data, total: data.length });
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.get('/health', async (req, res) => {
try {
await mediaModel.checkHealth();
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// 下载数据 API
router.get('/download-data/:type', async (req, res) => {
try {
const { type } = req.params;
const { cultured_type = 'cultured' } = req.query;
// 定义允许的下载类型
const allowedTypes = ['all_data', 'ph', 'temperature', 'oxygen', 'culture_medium', 'max_growth_rate'];
if (!allowedTypes.includes(type)) {
return res.status(400).json({
success: false,
message: 'Invalid download type',
allowedTypes: allowedTypes
});
}
// 根据类型定义需要查询的字段
let selectFields = '';
let headers = [];
switch (type) {
case 'all_data':
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species,
m.taxonomy,
m.pH as pH,
m.temperature as temperature,
m.o2 as o2,
m.nutrition,
m.gram_stain,
m.synonym,
m.media_name,
m.media_source,
m.mediadive_id,
TRIM(CASE
WHEN m.predict_pH IS NOT NULL AND m.predict_pH != '' THEN
SUBSTRING(m.predict_pH, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_pH)) > 0 THEN LENGTH(m.predict_pH) - LOCATE(' ', REVERSE(m.predict_pH)) ELSE LENGTH(m.predict_pH) END)
ELSE NULL
END) as ppH,
TRIM(CASE
WHEN m.predict_temperature IS NOT NULL AND m.predict_temperature != '' THEN
SUBSTRING(m.predict_temperature, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_temperature)) > 0 THEN LENGTH(m.predict_temperature) - LOCATE(' ', REVERSE(m.predict_temperature)) ELSE LENGTH(m.predict_temperature) END)
ELSE NULL
END) as ptemperature,
TRIM(CASE
WHEN m.predict_o2 IS NOT NULL AND m.predict_o2 != '' THEN
SUBSTRING(m.predict_o2, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_o2)) > 0 THEN LENGTH(m.predict_o2) - LOCATE(' ', REVERSE(m.predict_o2)) ELSE LENGTH(m.predict_o2) END)
ELSE NULL
END) as po2
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'taxonomy', 'pH', 'temperature', 'o2', 'nutrition', 'gram_stain', 'synonym', 'media_name', 'media_source', 'mediadive_id', 'predict_pH', 'predict_temperature', 'predict_o2'];
break;
case 'ph':
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species,
m.pH as pH,
TRIM(CASE
WHEN m.predict_pH IS NOT NULL AND m.predict_pH != '' THEN
SUBSTRING(m.predict_pH, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_pH)) > 0 THEN LENGTH(m.predict_pH) - LOCATE(' ', REVERSE(m.predict_pH)) ELSE LENGTH(m.predict_pH) END)
ELSE NULL
END) as ppH
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'pH', 'predict_pH'];
break;
case 'temperature':
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species,
m.temperature as temperature,
TRIM(CASE
WHEN m.predict_temperature IS NOT NULL AND m.predict_temperature != '' THEN
SUBSTRING(m.predict_temperature, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_temperature)) > 0 THEN LENGTH(m.predict_temperature) - LOCATE(' ', REVERSE(m.predict_temperature)) ELSE LENGTH(m.predict_temperature) END)
ELSE NULL
END) as ptemperature
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'temperature', 'predict_temperature'];
break;
case 'oxygen':
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species,
m.o2 as o2,
TRIM(CASE
WHEN m.predict_o2 IS NOT NULL AND m.predict_o2 != '' THEN
SUBSTRING(m.predict_o2, 1, CASE WHEN LOCATE(' ', REVERSE(m.predict_o2)) > 0 THEN LENGTH(m.predict_o2) - LOCATE(' ', REVERSE(m.predict_o2)) ELSE LENGTH(m.predict_o2) END)
ELSE NULL
END) as po2
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'o2', 'predict_o2'];
break;
case 'culture_medium':
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species,
m.nutrition,
m.media_name,
m.media_source,
m.mediadive_id
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'nutrition', 'media_name', 'media_source', 'mediadive_id'];
break;
case 'max_growth_rate':
// 注意:如果数据库中没有 max_growth_rate 字段,这里需要根据实际情况调整
selectFields = `
m.species_dive_id,
m.microbial_name,
m.genome,
m.domain,
m.phylum,
m.class,
m.\`order\`,
m.family,
m.genus,
m.species
`;
headers = ['species_dive_id', 'microbial_name', 'genome', 'domain', 'phylum', 'class', 'order', 'family', 'genus', 'species'];
break;
}
// 为每种类型定义 GROUP BY 字段
let groupByFields = '';
switch (type) {
case 'all_data':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species, m.taxonomy, m.pH, m.temperature, m.o2, m.nutrition, m.gram_stain, m.synonym, m.media_name, m.media_source, m.mediadive_id, m.predict_pH, m.predict_temperature, m.predict_o2`;
break;
case 'ph':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species, m.pH, m.predict_pH`;
break;
case 'temperature':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species, m.temperature, m.predict_temperature`;
break;
case 'oxygen':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species, m.o2, m.predict_o2`;
break;
case 'culture_medium':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species, m.nutrition, m.media_name, m.media_source, m.mediadive_id`;
break;
case 'max_growth_rate':
groupByFields = `m.species_dive_id, m.microbial_name, m.genome, m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, m.species`;
break;
}
// 构建查询
const query = `
SELECT ${selectFields}
FROM cultured_data m
WHERE m.cultured_type = ?
GROUP BY ${groupByFields}
ORDER BY m.species_dive_id
`;
console.log('Execute download query for type:', type);
const [rows] = await pool.execute(query, [cultured_type]);
console.log('Download query result row count:', rows.length);
// 将数据转换为 CSV 格式
const escapeCSVValue = (value) => {
if (value === null || value === undefined) return '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
const csvLines = [
headers.join(','),
...rows.map(row => {
return headers.map(header => {
// 处理字段名映射(如 pH -> pH, ppH -> predict_pH
let value = row[header];
if (value === undefined && header === 'predict_pH') {
value = row['ppH'];
} else if (value === undefined && header === 'predict_temperature') {
value = row['ptemperature'];
} else if (value === undefined && header === 'predict_o2') {
value = row['po2'];
}
return escapeCSVValue(value);
}).join(',');
})
];
const csvContent = csvLines.join('\n');
// 设置响应头
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${type}.csv"`);
res.send(csvContent);
} catch (error) {
console.error('Error downloading data:', error);
res.status(500).json({
success: false,
message: 'Failed to download data',
error: error.message
});
}
});
module.exports = router;