first commit
This commit is contained in:
452
routes/router.js
Normal file
452
routes/router.js
Normal file
@@ -0,0 +1,452 @@
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user