first commit

This commit is contained in:
gzy
2025-12-16 11:39:15 +08:00
commit a3bdbee7c2
118 changed files with 34631 additions and 0 deletions

452
routes/router.js Normal file
View 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;