diff --git a/UI/src/views/HistoryView.vue b/UI/src/views/HistoryView.vue index b223167..d58b1b1 100644 --- a/UI/src/views/HistoryView.vue +++ b/UI/src/views/HistoryView.vue @@ -87,12 +87,12 @@
- {{ currentPrediction.meta?.product_name || currentPrediction.product_name || getProductName(currentPrediction.meta?.product_id) || '未知产品' }} + {{ currentPrediction.product_name || '未知产品' }} - {{ currentPrediction.meta?.model_type || currentPrediction.model_type || '未知模型' }} + {{ currentPrediction.model_type || '未知模型' }} - {{ currentPrediction.meta?.start_date || currentPrediction.start_date || (currentPrediction.data?.prediction_data?.[0]?.date) || 'N/A' }} - {{ formatDateTime(currentPrediction.meta?.created_at || currentPrediction.created_at) }} + {{ currentPrediction.start_date || (currentPrediction.prediction_data?.[0]?.date) || 'N/A' }} + {{ formatDateTime(currentPrediction.created_at) }}
@@ -106,7 +106,7 @@

预测趋势图

-
+
@@ -122,29 +122,29 @@
-
{{ currentPrediction?.data?.prediction_data ? calculateAverage(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}
+
{{ currentPrediction?.prediction_data ? calculateAverage(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}
-
{{ currentPrediction?.data?.prediction_data ? calculateMax(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}
+
{{ currentPrediction?.prediction_data ? calculateMax(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}
-
{{ currentPrediction?.data?.prediction_data ? calculateMin(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}
+
{{ currentPrediction?.prediction_data ? calculateMin(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}
- + @@ -152,7 +152,7 @@ - + @@ -160,7 +160,7 @@ - + @@ -248,7 +248,7 @@ import { ref, onMounted, reactive, watch, nextTick } from 'vue'; import axios from 'axios'; import { ElMessage, ElMessageBox } from 'element-plus'; import { QuestionFilled, Search, View, Delete, ArrowUp, ArrowDown, Minus, Download } from '@element-plus/icons-vue'; -import Chart from 'chart.js/auto'; // << 关键改动:导入Chart.js +import Chart from 'chart.js/auto'; import { computed, onUnmounted } from 'vue'; const loading = ref(false); @@ -257,10 +257,9 @@ const products = ref([]); const modelTypes = ref([]); const detailsVisible = ref(false); const currentPrediction = ref(null); -const rawResponseData = ref(null); -const showRawDataFlag = ref(false); +const chartCanvas = ref(null); // 新增 ref -let predictionChart = null; // << 关键改动:使用单个chart实例 +let predictionChart = null; let historyChart = null; const filters = reactive({ @@ -327,7 +326,6 @@ const handleCurrentChange = (page) => { fetchHistory(); }; -// 添加getProductName函数 const getProductName = (productId) => { if (!productId) return '未知产品'; const product = products.value.find(p => p.product_id === productId); @@ -338,68 +336,11 @@ const viewDetails = async (id) => { try { const response = await axios.get(`/api/prediction/history/${id}`); const responseData = response.data; - rawResponseData.value = JSON.stringify(responseData, null, 2); - if (responseData && responseData.status === 'success') { - // 后端已经返回标准格式的数据,直接使用 - // 但仍需确保数据结构的完整性 - - // 确保meta字段存在 - if (!responseData.meta) { - const historyRecord = history.value.find(item => item.id === id); - responseData.meta = { - product_name: historyRecord?.product_name || responseData.product_name || '未知产品', - product_id: historyRecord?.product_id || responseData.product_id || 'N/A', - model_type: historyRecord?.model_type || responseData.model_type || '未知模型', - start_date: historyRecord?.start_date || responseData.start_date || 'N/A', - created_at: historyRecord?.created_at || responseData.created_at || new Date().toISOString(), - future_days: historyRecord?.future_days || responseData.future_days || 7, - model_id: historyRecord?.model_id || responseData.model_id || 'N/A' - }; - } - - // 确保data字段结构完整 - if (!responseData.data) { - responseData.data = { - prediction_data: [], - history_data: [], - data: [] - }; - } - - // 确保各个数据数组存在 - if (!Array.isArray(responseData.data.prediction_data)) { - responseData.data.prediction_data = responseData.prediction_data || []; - } - - if (!Array.isArray(responseData.data.history_data)) { - responseData.data.history_data = responseData.history_data || []; - } - - if (!Array.isArray(responseData.data.data)) { - // 合并历史和预测数据 - const historyData = responseData.data.history_data || []; - const predictionData = responseData.data.prediction_data || []; - - // 为数据添加类型标记 - const markedHistoryData = historyData.map(item => ({ - ...item, - data_type: item.data_type || '历史销量' - })); - - const markedPredictionData = predictionData.map(item => ({ - ...item, - data_type: item.data_type || '预测销量' - })); - - responseData.data.data = [...markedHistoryData, ...markedPredictionData]; - } - - currentPrediction.value = responseData; + if (responseData && responseData.status === 'success' && responseData.data) { + currentPrediction.value = responseData.data; detailsVisible.value = true; - - console.log('预测详情数据:', responseData); - + console.log('标准化的预测详情数据:', currentPrediction.value); } else { ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误')); } @@ -425,13 +366,11 @@ const deleteHistory = (id) => { try { await axios.delete(`/api/prediction/history/${id}`); ElMessage.success('删除成功'); - fetchHistory(); // 重新加载数据 + fetchHistory(); } catch (error) { ElMessage.error('删除失败'); } - }).catch(() => { - // - }); + }).catch(() => {}); }; const formatDateTime = (isoString) => { @@ -462,400 +401,6 @@ const getModelTagType = (modelType) => { return types[modelType] || 'info'; }; -const showRawData = () => { - showRawDataFlag.value = !showRawDataFlag.value; -}; - -const useRawDataDirectly = () => { - try { - if (!rawResponseData.value) { - ElMessage.warning('没有原始数据可用'); - return; - } - - // 解析原始JSON字符串 - const rawData = JSON.parse(rawResponseData.value); - console.log('尝试直接使用原始数据:', rawData); - - // 创建一个新的预测数据对象 - const processedData = { - status: 'success', - meta: {}, - data: { - prediction_data: [], - history_data: [] - } - }; - - // 尝试提取元数据 - if (rawData.meta) { - processedData.meta = rawData.meta; - } else if (rawData.product_name || rawData.model_type) { - processedData.meta = { - product_name: rawData.product_name, - model_type: rawData.model_type, - created_at: rawData.created_at, - start_date: rawData.start_date, - future_days: rawData.future_days, - model_id: rawData.model_id - }; - } - - // 处理历史数据 - 直接从原始数据中提取 - if (rawData.data && rawData.data.history_data) { - console.log('从原始数据中提取历史数据'); - processedData.data.history_data = rawData.data.history_data.map(item => { - // 处理NaN值 - const newItem = {...item}; - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) { - newItem.sales = null; - } - return newItem; - }); - } else if (Array.isArray(rawData)) { - console.log('原始数据是数组,直接使用'); - processedData.data.history_data = rawData.map(item => { - const newItem = {...item}; - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) { - newItem.sales = null; - } - return newItem; - }); - } - - // 处理预测数据 - if (rawData.data && rawData.data.prediction_data) { - console.log('从原始数据中提取预测数据'); - processedData.data.prediction_data = rawData.data.prediction_data.map(item => { - // 处理NaN值 - const newItem = {...item}; - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) { - newItem.sales = null; - } - return newItem; - }); - } - - // 如果没有预测数据但有历史数据,生成一些预测数据 - if (processedData.data.history_data.length > 0 && processedData.data.prediction_data.length === 0) { - console.log('生成预测数据'); - // 复制最后7天的历史数据作为预测数据 - const lastWeekData = [...processedData.data.history_data] - .slice(-7) - .map(item => ({ - ...item, - data_type: '预测销量', - predicted_sales: item.sales, - // 将日期向后推7天 - date: new Date(new Date(item.date).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] - })); - - processedData.data.prediction_data = lastWeekData; - } - - console.log('处理后的数据:', processedData); - - // 更新当前预测数据 - currentPrediction.value = processedData; - - // 重新初始化图表 - nextTick(() => { - // initDetailsChart(); - }); - - ElMessage.success('已尝试直接使用原始数据'); - } catch (error) { - console.error('使用原始数据失败:', error); - ElMessage.error(`使用原始数据失败: ${error.message}`); - } -}; - -const exportRawData = () => { - if (!rawResponseData.value) { - ElMessage.warning('没有原始数据可导出'); - return; - } - - try { - // 创建Blob对象 - const blob = new Blob([rawResponseData.value], { type: 'application/json' }); - - // 创建下载链接 - const url = URL.createObjectURL(blob); - - // 创建a标签并模拟点击 - const link = document.createElement('a'); - link.href = url; - link.download = `prediction_raw_data_${new Date().getTime()}.json`; - document.body.appendChild(link); - link.click(); - - // 清理 - URL.revokeObjectURL(url); - document.body.removeChild(link); - - ElMessage.success('原始数据已导出为JSON文件'); - } catch (error) { - console.error('导出数据失败:', error); - ElMessage.error(`导出数据失败: ${error.message}`); - } -}; - -const handleFileChange = (file) => { - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - try { - const jsonData = e.target.result; - console.log('导入的JSON文件大小:', jsonData.length, '字节'); - - // 保存原始数据,不进行解析 - rawResponseData.value = jsonData; - - // 显示原始数据 - showRawDataFlag.value = true; - - // 尝试解析JSON数据,仅用于日志输出 - try { - // 使用自定义JSON解析函数,处理NaN值 - const parsedData = parseJSONWithNaN(jsonData); - console.log('解析JSON数据成功'); - - // 分析数据结构 - console.log('数据结构:', Object.keys(parsedData)); - - if (parsedData.data) { - console.log('data字段类型:', typeof parsedData.data); - - if (typeof parsedData.data === 'object') { - console.log('data字段结构:', Object.keys(parsedData.data)); - - if (parsedData.data.history_data) { - console.log('history_data类型:', typeof parsedData.data.history_data); - console.log('history_data是否为数组:', Array.isArray(parsedData.data.history_data)); - console.log('history_data长度:', Array.isArray(parsedData.data.history_data) ? parsedData.data.history_data.length : 'N/A'); - - if (Array.isArray(parsedData.data.history_data) && parsedData.data.history_data.length > 0) { - console.log('history_data第一项:', parsedData.data.history_data[0]); - } - } - - if (parsedData.data.prediction_data) { - console.log('prediction_data类型:', typeof parsedData.data.prediction_data); - console.log('prediction_data是否为数组:', Array.isArray(parsedData.data.prediction_data)); - console.log('prediction_data长度:', Array.isArray(parsedData.data.prediction_data) ? parsedData.data.prediction_data.length : 'N/A'); - - if (Array.isArray(parsedData.data.prediction_data) && parsedData.data.prediction_data.length > 0) { - console.log('prediction_data第一项:', parsedData.data.prediction_data[0]); - } - } - } else if (typeof parsedData.data === 'string') { - console.log('data字段是字符串,可能需要进一步解析'); - try { - // 使用自定义JSON解析函数,处理NaN值 - const nestedData = parseJSONWithNaN(parsedData.data); - console.log('嵌套数据结构:', Object.keys(nestedData)); - } catch (nestedError) { - console.error('解析嵌套数据失败:', nestedError); - } - } - } else if (Array.isArray(parsedData)) { - console.log('数据是数组,长度:', parsedData.length); - if (parsedData.length > 0) { - console.log('数组第一项:', parsedData[0]); - } - } - } catch (parseError) { - console.error('解析JSON数据失败:', parseError); - // 即使解析失败,我们仍然保留原始数据 - } - - ElMessage.success('JSON文件导入成功'); - } catch (error) { - console.error('读取JSON文件失败:', error); - ElMessage.error(`读取JSON文件失败: ${error.message}`); - } - }; - - reader.onerror = (error) => { - console.error('读取文件失败:', error); - ElMessage.error('读取文件失败'); - }; - - reader.readAsText(file.raw); -}; - -const useImportedData = () => { - if (!rawResponseData.value) { - ElMessage.warning('没有导入的数据可用'); - return; - } - - try { - // 解析JSON数据 - let importedData; - - try { - // 使用自定义JSON解析函数,处理NaN值 - importedData = parseJSONWithNaN(rawResponseData.value); - console.log('成功解析导入的数据'); - } catch (parseError) { - console.error('解析导入数据失败:', parseError); - ElMessage.error(`解析导入数据失败: ${parseError.message}`); - return; - } - - console.log('使用导入的数据:', importedData); - - // 创建一个新的预测数据对象 - const processedData = { - status: 'success', - meta: {}, - data: { - prediction_data: [], - history_data: [] - } - }; - - // 提取元数据 - if (importedData.meta) { - processedData.meta = importedData.meta; - } else if (importedData.product_name || importedData.model_type) { - processedData.meta = { - product_name: importedData.product_name, - model_type: importedData.model_type, - created_at: importedData.created_at, - start_date: importedData.start_date, - future_days: importedData.future_days, - model_id: importedData.model_id - }; - } - - // 提取历史数据 - if (importedData.data && importedData.data.history_data && Array.isArray(importedData.data.history_data)) { - console.log('从导入数据中提取历史数据,长度:', importedData.data.history_data.length); - - processedData.data.history_data = importedData.data.history_data.map(item => { - const newItem = {...item}; - // 处理NaN值 - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) { - newItem.sales = null; - } - return newItem; - }); - } else if (Array.isArray(importedData)) { - // 如果导入数据本身是数组,尝试作为历史数据使用 - console.log('导入数据是数组,尝试作为历史数据使用'); - processedData.data.history_data = importedData.map(item => { - const newItem = {...item}; - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) { - newItem.sales = null; - } - return newItem; - }); - } else if (typeof importedData.data === 'string') { - // 如果data字段是字符串,尝试解析 - console.log('导入数据的data字段是字符串,尝试解析'); - try { - // 使用自定义JSON解析函数,处理NaN值 - const nestedData = parseJSONWithNaN(importedData.data); - - if (Array.isArray(nestedData)) { - console.log('嵌套数据是数组,作为历史数据使用'); - processedData.data.history_data = nestedData; - } else if (nestedData.history_data) { - console.log('嵌套数据包含history_data字段'); - processedData.data.history_data = nestedData.history_data; - } - } catch (error) { - console.error('解析嵌套数据失败:', error); - } - } - - // 提取预测数据 - if (importedData.data && importedData.data.prediction_data && Array.isArray(importedData.data.prediction_data)) { - console.log('从导入数据中提取预测数据,长度:', importedData.data.prediction_data.length); - - processedData.data.prediction_data = importedData.data.prediction_data.map(item => { - const newItem = {...item}; - // 处理NaN值 - if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) { - newItem.predicted_sales = null; - } - if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) { - newItem.sales = null; - } - return newItem; - }); - } - - // 如果没有预测数据但有历史数据,生成一些预测数据 - if (processedData.data.history_data.length > 0 && processedData.data.prediction_data.length === 0) { - console.log('生成预测数据'); - // 复制最后7天的历史数据作为预测数据 - const lastWeekData = [...processedData.data.history_data] - .slice(-7) - .map(item => ({ - ...item, - data_type: '预测销量', - predicted_sales: item.sales, - // 将日期向后推7天 - date: new Date(new Date(item.date).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] - })); - - processedData.data.prediction_data = lastWeekData; - } - - console.log('处理后的数据:', processedData); - - // 检查是否有有效数据 - if (processedData.data.prediction_data.length === 0 && processedData.data.history_data.length === 0) { - console.warn('导入的数据中未找到有效的预测或历史数据'); - ElMessage.warning('导入的数据中未找到有效的预测或历史数据'); - return; - } - - // 更新当前预测数据 - currentPrediction.value = processedData; - - // 重新初始化图表 - nextTick(() => { - // initDetailsChart(); - }); - - ElMessage.success('已使用导入的数据更新图表'); - } catch (error) { - console.error('使用导入数据失败:', error); - ElMessage.error(`使用导入数据失败: ${error.message}`); - } -}; - -// 自定义JSON解析函数,处理NaN值 -const parseJSONWithNaN = (jsonString) => { - // 将JSON字符串中的NaN替换为null - const processedString = jsonString - .replace(/:NaN,/g, ':null,') - .replace(/:NaN}/g, ':null}'); - - return JSON.parse(processedString); -}; - const calculateAverage = (data) => { if (!data || data.length === 0) return 0; return data.reduce((sum, item) => sum + item.sales, 0) / data.length; @@ -951,25 +496,21 @@ watch(detailsVisible, (newVal) => { if (newVal && currentPrediction.value) { nextTick(() => { renderChart(); - // 可以在这里添加渲染第二个图表的逻辑 - // renderHistoryAnalysisChart(); }); } }); -// << 关键改动:从ProductPredictionView.vue复制并适应的renderChart函数 +// 参照 ProductPredictionView.vue 的实现进行重构 const renderChart = () => { - const chartCanvas = document.getElementById('fullscreen-prediction-chart-history'); - if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return; - + if (!chartCanvas.value || !currentPrediction.value) return; if (predictionChart) { predictionChart.destroy(); } const formatDate = (date) => new Date(date).toISOString().split('T')[0]; - const historyData = (currentPrediction.value.data.history_data || []).map(p => ({ ...p, date: formatDate(p.date) })); - const predictionData = (currentPrediction.value.data.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) })); + const historyData = (currentPrediction.value.history_data || []).map(p => ({ ...p, date: formatDate(p.date) })); + const predictionData = (currentPrediction.value.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) })); if (historyData.length === 0 && predictionData.length === 0) { ElMessage.warning('没有可用于图表的数据。'); @@ -977,20 +518,24 @@ const renderChart = () => { } const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort(); - const simplifiedLabels = allLabels.map(date => date.split('-')[2]); + const simplifiedLabels = allLabels.map(date => date.split('-').slice(1).join('/')); const historyMap = new Map(historyData.map(p => [p.date, p.sales])); - // 注意:这里使用 'sales' 字段,因为后端已经统一了 + // 关键修正:预测数据在后端返回时键名为 'sales' const predictionMap = new Map(predictionData.map(p => [p.date, p.sales])); const alignedHistorySales = allLabels.map(label => historyMap.get(label) ?? null); const alignedPredictionSales = allLabels.map(label => predictionMap.get(label) ?? null); + // 连接点逻辑 if (historyData.length > 0 && predictionData.length > 0) { const lastHistoryDate = historyData[historyData.length - 1].date; const lastHistoryValue = historyData[historyData.length - 1].sales; if (!predictionMap.has(lastHistoryDate)) { - alignedPredictionSales[allLabels.indexOf(lastHistoryDate)] = lastHistoryValue; + const lastHistoryIndex = allLabels.indexOf(lastHistoryDate); + if (lastHistoryIndex !== -1) { + alignedPredictionSales[lastHistoryIndex] = lastHistoryValue; + } } } @@ -1003,7 +548,7 @@ const renderChart = () => { subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`; } - predictionChart = new Chart(chartCanvas, { + predictionChart = new Chart(chartCanvas.value, { type: 'line', data: { labels: simplifiedLabels, @@ -1015,7 +560,6 @@ const renderChart = () => { backgroundColor: 'rgba(103, 194, 58, 0.2)', tension: 0.4, fill: true, - spanGaps: false, }, { label: '预测销量', @@ -1032,10 +576,15 @@ const renderChart = () => { responsive: true, maintainAspectRatio: false, plugins: { + legend: { + labels: { + color: 'white' + } + }, title: { display: true, - text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`, - color: '#ffffff', + text: `${currentPrediction.value.product_name || '未知产品'} - 销量预测趋势图`, + color: 'white', font: { size: 20, weight: 'bold', @@ -1044,7 +593,7 @@ const renderChart = () => { subtitle: { display: true, text: subtitleText, - color: '#6c757d', + color: 'white', font: { size: 14, }, @@ -1057,7 +606,11 @@ const renderChart = () => { x: { title: { display: true, - text: '日期 (日)' + text: '日期', + color: 'white' + }, + ticks: { + color: 'white' }, grid: { display: false @@ -1066,10 +619,14 @@ const renderChart = () => { y: { title: { display: true, - text: '销量' + text: '销量', + color: 'white' + }, + ticks: { + color: 'white' }, grid: { - color: '#e9e9e9', + color: 'rgba(255, 255, 255, 0.2)', drawBorder: false, }, beginAtZero: true @@ -1081,7 +638,7 @@ const renderChart = () => { const exportHistoryData = () => { if (!currentPrediction.value) return; - const data = currentPrediction.value.data.data; + const data = [...(currentPrediction.value.history_data || []).map(d => ({...d, data_type: '历史销量'})), ...(currentPrediction.value.prediction_data || []).map(d => ({...d, data_type: '预测销量'}))]; let csvContent = "日期,销量,数据类型\n"; data.forEach(row => { csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`; @@ -1090,7 +647,7 @@ const exportHistoryData = () => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - const { product_name, model_type, start_date } = currentPrediction.value.meta; + const { product_name, model_type, start_date } = currentPrediction.value; link.setAttribute('download', `历史预测_${product_name}_${model_type}_${start_date}.csv`); document.body.appendChild(link); link.click(); @@ -1192,4 +749,4 @@ onMounted(() => { font-size: 14px; margin-bottom: 5px; } - \ No newline at end of file + \ No newline at end of file diff --git a/fix_old_predictions.py b/fix_old_predictions.py new file mode 100644 index 0000000..9fc22a3 --- /dev/null +++ b/fix_old_predictions.py @@ -0,0 +1,123 @@ +import sys +import os +import json +import sqlite3 +import traceback + +# 确保脚本可以找到项目模块 +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.join(current_dir, 'server')) + +from predictors.model_predictor import load_model_and_predict +from init_multi_store_db import get_db_connection +from utils.model_manager import ModelManager +from api import CustomJSONEncoder + +def fix_old_prediction_data(): + """ + 遍历数据库中的历史预测记录,重新生成并覆盖被截断的预测数据文件。 + """ + print("开始修复历史预测数据...") + + conn = get_db_connection() + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + try: + cursor.execute("SELECT * FROM prediction_history ORDER BY id") + records = cursor.fetchall() + + if not records: + print("✅ 数据库中没有历史预测记录,无需修复。") + return + + print(f"发现 {len(records)} 条历史记录,开始逐一检查...") + + model_manager = ModelManager(os.path.join(current_dir, 'saved_models')) + fixed_count = 0 + skipped_count = 0 + error_count = 0 + + for record in records: + record_id = record['id'] + file_path = record['file_path'] + future_days_db = record['future_days'] + + try: + if not file_path or not os.path.exists(file_path): + print(f"跳过记录 {record_id}: 文件不存在 at {file_path}") + skipped_count += 1 + continue + + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 兼容旧数据格式 + prediction_data = data.get('prediction_data', data.get('predictions', [])) + + if len(prediction_data) < future_days_db: + print(f"记录 {record_id} 需要修复 (文件: {len(prediction_data)}天, 数据库: {future_days_db}天). 开始重新生成...") + + # 从 model_id 解析所需信息 + model_id_parts = record['model_id'].split('_') + version = model_id_parts[-1] + + # 使用 list_models 查找对应的模型文件 + models_result = model_manager.list_models( + product_id=record['product_id'], + model_type=record['model_type'] + ) + + models = models_result.get('models', []) + found_model = None + for model in models: + if model.get('version') == version: + found_model = model + break + + if not found_model: + print(f"❌ 无法找到用于修复的模型文件: product={record['product_id']}, type={record['model_type']}, version={version}") + error_count += 1 + continue + + model_path = found_model['file_path'] + + # 重新生成预测 + new_prediction_result = load_model_and_predict( + model_path=model_path, + product_id=record['product_id'], + model_type=record['model_type'], + version=version, + future_days=future_days_db, + start_date=record['start_date'], + history_lookback_days=30 # 使用旧的默认值 + ) + + if new_prediction_result: + # 覆盖旧的JSON文件 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(new_prediction_result, f, ensure_ascii=False, indent=4, cls=CustomJSONEncoder) + print(f"成功修复并覆盖文件: {file_path}") + fixed_count += 1 + else: + print(f"修复记录 {record_id} 失败: 预测函数返回空结果。") + error_count += 1 + else: + skipped_count += 1 + + except Exception as e: + print(f"处理记录 {record_id} 时发生错误: {e}") + traceback.print_exc() + error_count += 1 + + print("\n--- 修复完成 ---") + print(f"总记录数: {len(records)}") + print(f"已修复: {fixed_count}") + print(f"已跳过 (无需修复): {skipped_count}") + print(f"失败: {error_count}") + + finally: + conn.close() + +if __name__ == '__main__': + fix_old_prediction_data() \ No newline at end of file diff --git a/prediction_history.db b/prediction_history.db index fe92e74..87bd123 100644 Binary files a/prediction_history.db and b/prediction_history.db differ diff --git a/server/api.py b/server/api.py index 34cce8e..79c09ec 100644 --- a/server/api.py +++ b/server/api.py @@ -1370,10 +1370,10 @@ def predict(): data = request.json model_type = data.get('model_type') version = data.get('version') - future_days = int(data.get('future_days', 7)) + future_days = int(data['future_days']) start_date = data.get('start_date', '') include_visualization = data.get('include_visualization', False) - history_lookback_days = int(data.get('history_lookback_days', 30)) # 新增参数 + history_lookback_days = int(data['history_lookback_days']) # 确定训练模式和标识符 training_mode = data.get('training_mode', 'product') @@ -3096,18 +3096,6 @@ def save_prediction_result(prediction_result, product_id, product_name, model_ty # 确保目录存在 os.makedirs('static/predictions', exist_ok=True) - # 限制数据量 - if 'history_data' in prediction_result and isinstance(prediction_result['history_data'], list): - history_data = prediction_result['history_data'] - if len(history_data) > 30: - print(f"保存时历史数据超过30天,进行裁剪,原始数量: {len(history_data)}") - prediction_result['history_data'] = history_data[-30:] # 只保留最近30天 - - if 'prediction_data' in prediction_result and isinstance(prediction_result['prediction_data'], list): - prediction_data = prediction_result['prediction_data'] - if len(prediction_data) > 7: - print(f"保存时预测数据超过7天,进行裁剪,原始数量: {len(prediction_data)}") - prediction_result['prediction_data'] = prediction_data[:7] # 只保留前7天 # 处理预测结果中可能存在的NumPy类型 def convert_numpy_types(obj): diff --git a/server/models/__pycache__/mlstm_model.cpython-313.pyc b/server/models/__pycache__/mlstm_model.cpython-313.pyc index 219b70d..ffc7b1c 100644 Binary files a/server/models/__pycache__/mlstm_model.cpython-313.pyc and b/server/models/__pycache__/mlstm_model.cpython-313.pyc differ diff --git a/server/models/__pycache__/slstm_model.cpython-313.pyc b/server/models/__pycache__/slstm_model.cpython-313.pyc index edbd7f5..f5e59b9 100644 Binary files a/server/models/__pycache__/slstm_model.cpython-313.pyc and b/server/models/__pycache__/slstm_model.cpython-313.pyc differ diff --git a/server/models/__pycache__/transformer_model.cpython-313.pyc b/server/models/__pycache__/transformer_model.cpython-313.pyc index 4f85624..3659ee9 100644 Binary files a/server/models/__pycache__/transformer_model.cpython-313.pyc and b/server/models/__pycache__/transformer_model.cpython-313.pyc differ