// mediaModel.js const { pool } = require('../config/db'); // 假设你的db配置路径 // --- 内部辅助函数:构建 WHERE 子句 --- // 这个函数被 browse, sunburst, sankey 多个接口复用 const buildWhereClause = (filters) => { let whereConditions = []; let queryParams = []; const { search, ph, temperature, o2, taxonomy, taxonomyValue, microbial, cultured_type } = filters; // 1. Cultured Type if (cultured_type) { whereConditions.push('m.cultured_type = ?'); queryParams.push(cultured_type); } // 2. Search (通用搜索) if (search) { whereConditions.push('(m.microbial_name LIKE ? OR m.taxonomy LIKE ?)'); queryParams.push(`%${search}%`, `%${search}%`); } // 3. pH Range (处理预测值逻辑) if (ph) { const phExpr = `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)`; if (ph.includes('-')) { const [min, max] = ph.split('-').map(Number); whereConditions.push(`${phExpr} IS NOT NULL AND CAST(${phExpr} AS DECIMAL(4,2)) >= ? AND CAST(${phExpr} AS DECIMAL(4,2)) <= ?`); queryParams.push(min, max); } else { const val = parseFloat(ph); whereConditions.push(`${phExpr} IS NOT NULL AND CAST(${phExpr} AS DECIMAL(4,2)) >= ? AND CAST(${phExpr} AS DECIMAL(4,2)) <= ?`); queryParams.push(val - 0.5, val + 0.5); } } // 4. Temperature (处理预测值逻辑) if (temperature) { const tempExpr = `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)`; whereConditions.push(`${tempExpr} = ?`); queryParams.push(temperature); } // 5. O2 (处理预测值逻辑) if (o2) { const o2Expr = `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)`; whereConditions.push(`${o2Expr} LIKE ?`); queryParams.push(`%${o2}%`); } // 6. Taxonomy if (taxonomy && taxonomy !== 'ALL' && taxonomy !== '') { const validLevels = ['Bacteria', 'Domain', 'Phylum', 'Class', 'Order', 'Family', 'Genus', 'Species']; if (validLevels.includes(taxonomy)) { let field = taxonomy === 'Bacteria' ? 'm.domain' : `m.${taxonomy.toLowerCase()}`; // 特殊处理 Order 关键字 if (taxonomy.toLowerCase() === 'order') field = 'm.`order`'; if (taxonomyValue && taxonomyValue.trim() !== '') { whereConditions.push(`${field} LIKE ?`); queryParams.push(`%${taxonomyValue.trim()}%`); } else { whereConditions.push(`${field} IS NOT NULL AND ${field} != ''`); } } } // 7. Microbial Name if (microbial && microbial.trim() !== '') { whereConditions.push('m.microbial_name LIKE ?'); queryParams.push(`%${microbial.trim()}%`); } return { clause: whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '', params: queryParams }; }; // --- 数据清洗辅助函数 --- const extractTemperatureCategory = (str) => str ? str.split(',')[0].trim() : ''; const extractO2Category = (str) => str ? str.split(' ')[0] : ''; // --- 核心 Model 对象 --- const mediaModel = { // 1. 获取总数 async getTotalCount(filters) { const { clause, params } = buildWhereClause(filters); const query = `SELECT COUNT(DISTINCT m.species_dive_id) as total FROM cultured_data m ${clause}`; const [result] = await pool.execute(query, params); return result[0].total; }, // 2. 获取浏览列表数据 async getBrowseData(filters, page, pageSize) { const { clause, params } = buildWhereClause(filters); const offset = (page - 1) * pageSize; // 处理排序参数 const { sortBy, sortOrder } = filters; let orderByClause = 'ORDER BY m.species_dive_id'; if (sortBy && sortOrder) { // 定义允许排序的字段映射 const sortFieldMap = { 'microbial': 'm.microbial_name', 'nutrition': 'm.nutrition', 'domain': 'm.domain', 'ph': 'm.pH', 'temperature': 'm.temperature', 'o2': 'm.o2' }; const sortField = sortFieldMap[sortBy.toLowerCase()]; if (sortField) { const direction = sortOrder.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; orderByClause = `ORDER BY ${sortField} ${direction}`; } } const query = ` SELECT m.species_dive_id, m.microbial_name, m.nutrition, m.domain as domain, m.pH as pH, m.temperature as temperature, m.o2 as o2, m.predict_pH as ppH_raw, m.predict_temperature as ptemperature_raw, m.predict_o2 as po2_raw, -- Extract predicted value of predict_pH (before the last space) CASE WHEN m.predict_pH IS NOT NULL AND m.predict_pH != '' THEN TRIM(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, -- Extract probability of predict_pH (after the last space) CASE WHEN m.predict_pH IS NOT NULL AND m.predict_pH != '' AND LOCATE(' ', REVERSE(m.predict_pH)) > 0 THEN TRIM(SUBSTRING(m.predict_pH, LENGTH(m.predict_pH) - LOCATE(' ', REVERSE(m.predict_pH)) + 2)) ELSE NULL END as ppH_probability, -- Extract predicted value of predict_temperature (before the last space) CASE WHEN m.predict_temperature IS NOT NULL AND m.predict_temperature != '' THEN TRIM(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, -- Extract probability of predict_temperature (after the last space) CASE WHEN m.predict_temperature IS NOT NULL AND m.predict_temperature != '' AND LOCATE(' ', REVERSE(m.predict_temperature)) > 0 THEN TRIM(SUBSTRING(m.predict_temperature, LENGTH(m.predict_temperature) - LOCATE(' ', REVERSE(m.predict_temperature)) + 2)) ELSE NULL END as ptemperature_probability, -- Extract predicted value of predict_o2 (before the last space) CASE WHEN m.predict_o2 IS NOT NULL AND m.predict_o2 != '' THEN TRIM(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, -- Extract probability of predict_o2 (after the last space) CASE WHEN m.predict_o2 IS NOT NULL AND m.predict_o2 != '' AND LOCATE(' ', REVERSE(m.predict_o2)) > 0 THEN TRIM(SUBSTRING(m.predict_o2, LENGTH(m.predict_o2) - LOCATE(' ', REVERSE(m.predict_o2)) + 2)) ELSE NULL END as po2_probability, m.genome FROM cultured_data m ${clause} GROUP BY m.species_dive_id, m.microbial_name, m.nutrition, m.domain, m.pH, m.temperature, m.o2, m.predict_pH, m.predict_temperature, m.predict_o2, m.genome ${orderByClause} LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)} `; const [rows] = await pool.execute(query, params); // 格式化返回 return rows.map(row => ({ id: row.species_dive_id, microbial_name: row.microbial_name || '', nutrition: row.nutrition || '', domain: row.domain || '', pH: row.ppH ? row.ppH.toString() : (row.pH ? row.pH.toString() : ''), pH_probability: row.ppH_probability || null, temperature: row.ptemperature ? extractTemperatureCategory(row.ptemperature.split(',')[0].trim()) : (row.temperature ? extractTemperatureCategory(row.temperature.split(',')[0].trim()) : ''), temperature_probability: row.ptemperature_probability || null, o2: row.po2 ? extractO2Category(row.po2) : (row.o2 ? extractO2Category(row.o2) : ''), o2_probability: row.po2_probability || null, ppH: row.ppH || null, ptemperature: row.ptemperature || null, po2: row.po2 || null })); }, // 3. 微生物详情 async getMicrobialDetail(name, level = 'genus') { if (!name) { throw new Error('Microbial name cannot be empty'); } console.log('Get microbial detail information:', name, 'Taxonomy Level:', level); // 定义允许的层级 const allowedLevels = ['genus', 'family', 'order', 'class', 'phylum']; const safeLevel = allowedLevels.includes(level.toLowerCase()) ? level.toLowerCase() : 'genus'; // 构建基础查询字段 const baseFields = ` m.species_dive_id, m.microbial_name, 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.gram_stain, m.synonym, m.genome, m.nutrition, m.cultured_type, 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, CASE WHEN m.predict_pH IS NOT NULL AND m.predict_pH != '' AND LOCATE(' ', REVERSE(m.predict_pH)) > 0 THEN TRIM(SUBSTRING(m.predict_pH, LENGTH(m.predict_pH) - LOCATE(' ', REVERSE(m.predict_pH)) + 2)) ELSE NULL END as ppH_probability, 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, CASE WHEN m.predict_temperature IS NOT NULL AND m.predict_temperature != '' AND LOCATE(' ', REVERSE(m.predict_temperature)) > 0 THEN TRIM(SUBSTRING(m.predict_temperature, LENGTH(m.predict_temperature) - LOCATE(' ', REVERSE(m.predict_temperature)) + 2)) ELSE NULL END as ptemperature_probability, 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, CASE WHEN m.predict_o2 IS NOT NULL AND m.predict_o2 != '' AND LOCATE(' ', REVERSE(m.predict_o2)) > 0 THEN TRIM(SUBSTRING(m.predict_o2, LENGTH(m.predict_o2) - LOCATE(' ', REVERSE(m.predict_o2)) + 2)) ELSE NULL END as po2_probability `; // 首先尝试精确匹配 let detailQuery = ` SELECT ${baseFields} FROM cultured_data m WHERE m.microbial_name = ? LIMIT 1 `; console.log('Execute exact match query:', detailQuery, 'Parameters:', [name]); let [rows] = await pool.execute(detailQuery, [name]); console.log('Exact match query result:', rows.length, 'records'); // 如果精确匹配失败,尝试模糊匹配 if (rows.length === 0) { detailQuery = ` SELECT ${baseFields} FROM cultured_data m WHERE m.microbial_name LIKE ? LIMIT 1 `; console.log('Execute fuzzy match query:', detailQuery, 'Parameters:', [`%${name}%`]); [rows] = await pool.execute(detailQuery, [`%${name}%`]); console.log('Fuzzy match query result:', rows.length, 'records'); } if (rows.length === 0) { return null; // 返回 null 表示未找到 } const microbialData = rows[0]; console.log('Found microbial data:', microbialData.microbial_name, 'Genus:', microbialData.genus); // 获取 nutrition 信息 let nutritionInfo = microbialData.nutrition || ''; // 查询相关微生物(基于指定的层级) let relatedRows = []; const levelValue = microbialData[safeLevel]; // 获取指定层级的值 if (levelValue) { try { // 处理 'order' 关键字 const columnRef = safeLevel === 'order' ? '`order`' : safeLevel; const relatedQuery = ` SELECT ${baseFields} FROM cultured_data m WHERE m.${columnRef} = ? AND m.microbial_name != ? ORDER BY m.microbial_name `; console.log('Execute related microbes query:', relatedQuery, 'Parameters:', [levelValue, microbialData.microbial_name]); const [relatedResult] = await pool.execute(relatedQuery, [levelValue, microbialData.microbial_name]); relatedRows = relatedResult; console.log('Related microbes query result:', relatedRows.length, 'records'); } catch (relatedError) { console.error('Related microbes query failed:', relatedError); // 继续执行,使用空数组 } } // 构建并返回格式化数据 const responseData = { ...microbialData, // 保存原始值 pH: microbialData.pH, temperature: microbialData.temperature, o2: microbialData.o2, // 保存预测值(如果存在) ppH: microbialData.ppH || null, ptemperature: microbialData.ptemperature || null, po2: microbialData.po2 || null, // 保存预测概率(如果存在) ppH_probability: microbialData.ppH_probability || null, ptemperature_probability: microbialData.ptemperature_probability || null, po2_probability: microbialData.po2_probability || null, nutrition: nutritionInfo, culture_type: microbialData.cultured_type, related_microbes: relatedRows.map(row => ({ ...row, // 保存原始值 pH: row.pH, temperature: row.temperature, o2: row.o2, // 保存预测值(如果存在) ppH: row.ppH || null, ptemperature: row.ptemperature || null, po2: row.po2 || null, // 保存预测概率(如果存在) ppH_probability: row.ppH_probability || null, ptemperature_probability: row.ptemperature_probability || null, po2_probability: row.po2_probability || null })) }; console.log('Return data:', { microbial_name: responseData.microbial_name, level: safeLevel, levelValue: levelValue, related_count: responseData.related_microbes.length }); return responseData; }, // 4. 获取 Sankey 数据 async getSankeyData(filters) { console.log('开始构建6层桑基图数据...'); const { clause, params } = buildWhereClause(filters); try { // 查询完整的6层分类层级数据 const hierarchyQuery = ` SELECT m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus, COUNT(DISTINCT m.species_dive_id) as species_count FROM cultured_data m ${clause ? clause + ' AND' : 'WHERE'} m.domain IS NOT NULL AND m.domain != '' AND m.phylum IS NOT NULL AND m.phylum != '' AND m.class IS NOT NULL AND m.class != '' AND m.\`order\` IS NOT NULL AND m.\`order\` != '' AND m.family IS NOT NULL AND m.family != '' AND m.genus IS NOT NULL AND m.genus != '' GROUP BY m.domain, m.phylum, m.class, m.\`order\`, m.family, m.genus ORDER BY species_count DESC `; console.log('执行6层分类查询:', hierarchyQuery); const [hierarchyRows] = await pool.execute(hierarchyQuery, params); console.log('查询结果:', hierarchyRows.length, '条记录'); if (hierarchyRows.length === 0) { console.log('没有找到数据,返回空桑基图'); return { nodes: [], links: [] }; } // 定义层级顺序 const levelOrder = ['domain', 'phylum', 'class', 'order', 'family', 'genus']; // 构建每个层级的分类统计 const levelCounts = new Map(); // 存储每个分类的物种总数 const parentChildMap = new Map(); // 存储父子关系映射 // 处理每条完整的分类路径 for (const row of hierarchyRows) { const speciesCount = parseInt(row.species_count); // 为每个层级的分类累加物种数量 for (let i = 0; i < levelOrder.length; i++) { const level = levelOrder[i]; const categoryName = row[level]; if (categoryName) { const key = `${level}:${categoryName}`; levelCounts.set(key, (levelCounts.get(key) || 0) + speciesCount); // 建立父子关系 if (i > 0) { const parentLevel = levelOrder[i - 1]; const parentName = row[parentLevel]; if (parentName) { const parentKey = `${parentLevel}:${parentName}`; const childKey = `${level}:${categoryName}`; if (!parentChildMap.has(parentKey)) { parentChildMap.set(parentKey, new Set()); } parentChildMap.get(parentKey).add(childKey); } } } } } console.log('层级统计完成:', { levelCountsSize: levelCounts.size, parentChildMapSize: parentChildMap.size }); // 构建节点 - 每层最多10个节点,确保节点名称唯一 const nodes = []; const nodeMap = new Map(); let nodeIndex = 0; // 计算总物种数量 const totalSpeciesCount = hierarchyRows.reduce((sum, row) => sum + parseInt(row.species_count), 0); // 添加Total节点 nodes.push({ name: "Total", level: "total", levelName: "总计", depth: 0, // Total节点在第0层 itemStyle: { color: '#5470c6' } }); nodeMap.set("Total", nodeIndex++); // 为每个层级选择前10个最多的分类 const selectedCategories = new Map(); // 存储每层选中的分类 // 颜色方案和层级中文名称 const levelColors = { 'domain': '#91cc75', 'phylum': '#fac858', 'class': '#ee6666', 'order': '#73c0de', 'family': '#3ba272', 'genus': '#fc8452' }; const levelNames = { 'domain': 'domain', 'phylum': 'phylum', 'class': 'class', 'order': 'order', 'family': 'family', 'genus': 'genus' }; // 层级深度映射 const levelDepth = { 'domain': 1, 'phylum': 2, 'class': 3, 'order': 4, 'family': 5, 'genus': 6 }; for (const level of levelOrder) { // 获取当前层级的所有分类,按物种数量排序 const levelCategories = []; for (const [key, count] of levelCounts) { if (key.startsWith(`${level}:`)) { const categoryName = key.substring(level.length + 1); levelCategories.push({ name: categoryName, count: count, key: key }); } } // 按物种数量降序排序,根据层级决定选择数量 levelCategories.sort((a, b) => b.count - a.count); // family和genus层级选择前20个,其他层级选择前10个 const maxNodes = (level === 'family' || level === 'genus') ? 20 : 10; const topCategories = levelCategories.slice(0, maxNodes); console.log(`${level}层级: 总共${levelCategories.length}个分类,选择前${topCategories.length}个`); // 存储选中的分类 const selectedKeys = new Set(); for (const category of topCategories) { selectedKeys.add(category.key); // 为节点添加层级信息,包括depth属性 nodes.push({ name: category.name, level: level, levelName: levelNames[level], depth: levelDepth[level], // 添加depth属性指定层级 count: category.count, itemStyle: { color: levelColors[level] || '#5470c6' } }); nodeMap.set(category.key, nodeIndex++); } selectedCategories.set(level, selectedKeys); } console.log('节点构建完成:', { totalNodes: nodes.length, nodesByLevel: Object.fromEntries( levelOrder.map(level => [level, selectedCategories.get(level).size]) ) }); // 对每个depth层级的节点value进行归一化 // 先计算每个节点的初始value(基于count),再按层级归一化 const BASE_VALUE = 1000; // 每层的基准总值 const depthGroups = {}; // 按depth分组节点 // 将节点按depth分组,并初始化value for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const depth = node.depth; if (!depthGroups[depth]) { depthGroups[depth] = []; } depthGroups[depth].push(i); // 存储节点索引 // 初始化value为count nodes[i].value = nodes[i].count || 0; } // 设置Total节点的value为BASE_VALUE nodes[0].value = BASE_VALUE; // 构建连接 - 确保连接完整性 const links = []; // 1. Total到domain层的连接(使用比例关系) let domainTotalCount = 0; const domainNodes = []; for (const [key, count] of levelCounts) { if (key.startsWith('domain:') && nodeMap.has(key)) { const targetIndex = nodeMap.get(key); domainNodes.push({ index: targetIndex, count: count }); domainTotalCount += count; } } // 按比例从Total分配到domain for (const domainNode of domainNodes) { const ratio = domainNode.count / domainTotalCount; links.push({ source: 0, // Total的索引 target: domainNode.index, value: BASE_VALUE * ratio }); } // 2. 相邻层级间的连接 for (let i = 0; i < levelOrder.length - 1; i++) { const currentLevel = levelOrder[i]; const nextLevel = levelOrder[i + 1]; const currentSelected = selectedCategories.get(currentLevel); const nextSelected = selectedCategories.get(nextLevel); // 为每个父子关系创建连接 for (const [parentKey, children] of parentChildMap) { if (parentKey.startsWith(`${currentLevel}:`) && currentSelected.has(parentKey) && nodeMap.has(parentKey)) { const parentIndex = nodeMap.get(parentKey); // 计算该父节点下所有选中子节点的count总和 let childrenTotalCount = 0; const validChildren = []; for (const childKey of children) { if (childKey.startsWith(`${nextLevel}:`) && nextSelected.has(childKey) && nodeMap.has(childKey)) { const childCount = levelCounts.get(childKey) || 0; childrenTotalCount += childCount; validChildren.push({ key: childKey, count: childCount, index: nodeMap.get(childKey) }); } } // 存储父节点的流出比例信息 for (const child of validChildren) { const ratio = childrenTotalCount > 0 ? child.count / childrenTotalCount : 0; links.push({ source: parentIndex, target: child.index, ratio: ratio, // 暂存比例,后续计算value childCount: child.count }); } } } } console.log('连接关系构建完成:', links.length); // 3. 从Total开始,逐层计算value // 先计算每个节点的流入value const nodeInflow = new Array(nodes.length).fill(0); // 第一层:Total -> domain for (const link of links) { if (link.source === 0 && link.value !== undefined) { nodeInflow[link.target] += link.value; } } // 更新domain节点的value for (const domainNode of domainNodes) { nodes[domainNode.index].value = nodeInflow[domainNode.index]; } // 后续层级:根据父节点的value和比例计算连接value for (const link of links) { if (link.source !== 0 && link.ratio !== undefined) { const parentValue = nodes[link.source].value; link.value = parentValue * link.ratio; nodeInflow[link.target] += link.value; // 更新目标节点的value nodes[link.target].value = nodeInflow[link.target]; } } // 清理ratio字段 for (const link of links) { delete link.ratio; delete link.childCount; } // 4. 对每层进行归一化调整 for (const depth in depthGroups) { if (depth === '0') continue; // 跳过Total const nodeIndices = depthGroups[depth]; let currentTotal = 0; for (const idx of nodeIndices) { currentTotal += nodes[idx].value || 0; } // 归一化系数 const normalizationFactor = currentTotal > 0 ? BASE_VALUE / currentTotal : 1; // 调整该层所有节点的value for (const idx of nodeIndices) { nodes[idx].value = (nodes[idx].value || 0) * normalizationFactor; } // 同时调整流入该层节点的连接value for (const link of links) { if (nodeIndices.includes(link.target)) { link.value = (link.value || 0) * normalizationFactor; } } console.log(`Depth ${depth} 归一化: 原始total=${currentTotal.toFixed(2)}, 归一化后=${BASE_VALUE}, 系数=${normalizationFactor.toFixed(4)}`); } // 验证每层的总value const depthTotals = {}; for (const node of nodes) { const depth = node.depth; if (!depthTotals[depth]) { depthTotals[depth] = 0; } depthTotals[depth] += node.value; } console.log('最终每层节点value总和:', depthTotals); // 验证数据完整性 const invalidLinks = links.filter(link => link.source === undefined || link.target === undefined || link.value === undefined || isNaN(link.source) || isNaN(link.target) || isNaN(link.value) || link.source >= nodes.length || link.target >= nodes.length || link.source < 0 || link.target < 0 ); if (invalidLinks.length > 0) { console.error('发现无效连接:', invalidLinks); // 过滤掉无效连接 const validLinks = links.filter(link => link.source !== undefined && link.target !== undefined && link.value !== undefined && !isNaN(link.source) && !isNaN(link.target) && !isNaN(link.value) && link.source >= 0 && link.target >= 0 && link.source < nodes.length && link.target < nodes.length ); console.log('过滤后的有效连接数量:', validLinks.length); return { nodes: nodes, links: validLinks }; } console.log('6层桑基图数据构建完成:', { nodesCount: nodes.length, linksCount: links.length, totalSpeciesCount: totalSpeciesCount, sampleNodes: nodes.slice(0, 3).map((n, i) => ({index: i, ...n})), sampleLinks: links.slice(0, 3) }); // 详细验证数据结构 console.log('=== Data validation ==='); console.log('All nodes:', nodes.map((n, i) => `[${i}] ${n.name} (${n.levelName || n.level}) - counts:${n.count || 'N/A'}` ).slice(0, 20).join(', ') + (nodes.length > 20 ? ` ... ${nodes.length}nodes in total` : '')); console.log('Top 10 connections:', links.slice(0, 10).map(l => { const source = nodes[l.source]; const target = nodes[l.target]; return `${source?.name}(${source?.levelName})[${l.source}] -> ${target?.name}(${target?.levelName})[${l.target}] (${(l.value * 100).toFixed(2)}%)`; })); // 检查是否有重复的节点名称 const nodeNames = nodes.map(n => n.name); const duplicates = nodeNames.filter((name, index) => nodeNames.indexOf(name) !== index); if (duplicates.length > 0) { console.warn('Warning: Duplicate node names detected:', [...new Set(duplicates)]); } return { nodes: nodes, links: links }; } catch (error) { console.error('An error occurred while constructing the Sankey chart data:', error); return { nodes: [], links: [] }; } }, // 5. 统计数据 (Taxonomy) async getTaxonomyStats(level, cultured_type) { // 先获取总数 const countQuery = ` SELECT COUNT(DISTINCT \`${level}\`) as total_count FROM cultured_data m WHERE \`${level}\` IS NOT NULL AND \`${level}\` != '' AND m.cultured_type = ? `; const [countResult] = await pool.execute(countQuery, [cultured_type]); const total = countResult[0].total_count; // 获取详细数据 const query = ` SELECT \`${level}\` as name, COUNT(DISTINCT m.species_dive_id) as value FROM cultured_data m WHERE \`${level}\` IS NOT NULL AND \`${level}\` != '' AND m.cultured_type = ? GROUP BY \`${level}\` ORDER BY value DESC `; const [rows] = await pool.execute(query, [cultured_type]); const data = rows.map(r => ({ name: r.name, value: parseInt(r.value) })); return { data, total }; }, // 6. 营养统计 (包含复杂的 Python List 解析) async getNutritionStats(cultured_type) { const query = `SELECT m.nutrition, COUNT(DISTINCT m.species_dive_id) as count FROM cultured_data m WHERE m.nutrition IS NOT NULL AND m.nutrition != '' AND m.cultured_type = ? GROUP BY m.nutrition`; const [rows] = await pool.execute(query, [cultured_type]); const frequency = {}; let totalSpecies = 0; rows.forEach(row => { totalSpecies += parseInt(row.count); let str = row.nutrition.trim().replace(/^\[|\]$/g, ''); if(str) { const list = str.split(',').map(i => i.trim().replace(/^['"]|['"]$/g, '')).filter(i => i); list.forEach(nut => { frequency[nut] = (frequency[nut] || 0) + parseInt(row.count); }); } }); const data = Object.entries(frequency) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); return { data, total: data.length, totalSpecies }; }, // 7. 理化性质统计 async getPhyschemStats(type, cultured_type) { let data = {}; if (type === 'o2') { // O2 类型统计 const query = ` SELECT CASE WHEN m.o2 IS NOT NULL AND m.o2 != '' THEN SUBSTRING(m.o2, 1, CASE WHEN LOCATE(' ', m.o2) > 0 THEN LOCATE(' ', m.o2) - 1 ELSE LENGTH(m.o2) END) ELSE NULL END as name, COUNT(DISTINCT m.species_dive_id) as count FROM cultured_data m WHERE m.o2 IS NOT NULL AND m.o2 != '' AND m.cultured_type = ? GROUP BY CASE WHEN m.o2 IS NOT NULL AND m.o2 != '' THEN SUBSTRING(m.o2, 1, CASE WHEN LOCATE(' ', m.o2) > 0 THEN LOCATE(' ', m.o2) - 1 ELSE LENGTH(m.o2) END) ELSE NULL END ORDER BY count DESC `; const [rows] = await pool.execute(query, [cultured_type]); const total = rows.reduce((sum, row) => sum + parseInt(row.count || 0), 0); data = { aerobe: 0, anaerobe: 0, facultative: 0, total: total }; rows.forEach(row => { if (!row.name) return; // 跳过空值 const name = row.name.toLowerCase(); const count = parseInt(row.count || 0); if (name.includes('aerobe') && !name.includes('anaerobe')) { data.aerobe += count; } else if (name.includes('anaerobe')) { data.anaerobe += count; } else if (name.includes('facultative')) { data.facultative += count; } }); } else if (type === 'ph') { // pH 分布统计 const query = ` SELECT CASE WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 1 AND 2 THEN 1 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 2 AND 3 THEN 2 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 3 AND 4 THEN 3 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 4 AND 5 THEN 4 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 5 AND 6 THEN 5 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 6 AND 7 THEN 6 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 7 AND 8 THEN 7 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 8 AND 9 THEN 8 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 9 AND 10 THEN 9 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 10 AND 11 THEN 10 WHEN CAST(m.pH AS DECIMAL(4,2)) BETWEEN 11 AND 12 THEN 11 ELSE 12 END as ph_range, COUNT(DISTINCT m.species_dive_id) as count FROM cultured_data m WHERE m.pH IS NOT NULL AND m.pH != '' AND m.pH != 'Unknown' AND m.cultured_type = ? GROUP BY ph_range ORDER BY ph_range `; const [rows] = await pool.execute(query, [cultured_type]); data = rows.map(row => ({ x: parseInt(row.ph_range), count: parseInt(row.count) })); } else if (type === 'temperature') { // 温度类别分布统计 const query = ` SELECT CASE WHEN m.temperature IS NOT NULL AND m.temperature != '' THEN SUBSTRING(m.temperature, 1, CASE WHEN LOCATE(' ', m.temperature) > 0 THEN LOCATE(' ', m.temperature) - 1 ELSE LENGTH(m.temperature) END) ELSE NULL END as temp_category, COUNT(DISTINCT m.species_dive_id) as count FROM cultured_data m WHERE m.temperature IS NOT NULL AND m.temperature != '' AND m.temperature != 'Unknown' AND m.cultured_type = ? GROUP BY CASE WHEN m.temperature IS NOT NULL AND m.temperature != '' THEN SUBSTRING(m.temperature, 1, CASE WHEN LOCATE(' ', m.temperature) > 0 THEN LOCATE(' ', m.temperature) - 1 ELSE LENGTH(m.temperature) END) ELSE NULL END ORDER BY count DESC `; const [rows] = await pool.execute(query, [cultured_type]); data = rows.map(row => ({ x: row.temp_category || '', count: parseInt(row.count || 0) })); } return data; }, // 8. 健康检查 async checkHealth() { const conn = await pool.getConnection(); await conn.ping(); conn.release(); return true; } }; module.exports = mediaModel;