重构历史预测详情图表展示
This commit is contained in:
parent
08b26b5fa0
commit
aab685123b
@ -87,12 +87,12 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div class="prediction-summary">
|
<div class="prediction-summary">
|
||||||
<el-descriptions :column="4" border>
|
<el-descriptions :column="4" border>
|
||||||
<el-descriptions-item label="产品名称">{{ currentPrediction.meta?.product_name || currentPrediction.product_name || getProductName(currentPrediction.meta?.product_id) || '未知产品' }}</el-descriptions-item>
|
<el-descriptions-item label="产品名称">{{ currentPrediction.product_name || '未知产品' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="模型类型">
|
<el-descriptions-item label="模型类型">
|
||||||
<el-tag :type="getModelTagType(currentPrediction.meta?.model_type || currentPrediction.model_type)">{{ currentPrediction.meta?.model_type || currentPrediction.model_type || '未知模型' }}</el-tag>
|
<el-tag :type="getModelTagType(currentPrediction.model_type)">{{ currentPrediction.model_type || '未知模型' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="预测起始日">{{ currentPrediction.meta?.start_date || currentPrediction.start_date || (currentPrediction.data?.prediction_data?.[0]?.date) || 'N/A' }}</el-descriptions-item>
|
<el-descriptions-item label="预测起始日">{{ currentPrediction.start_date || (currentPrediction.prediction_data?.[0]?.date) || 'N/A' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.meta?.created_at || currentPrediction.created_at) }}</el-descriptions-item>
|
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.created_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -106,7 +106,7 @@
|
|||||||
<div class="prediction-chart-container">
|
<div class="prediction-chart-container">
|
||||||
<h3>预测趋势图</h3>
|
<h3>预测趋势图</h3>
|
||||||
<el-card shadow="hover" body-style="padding: 0;">
|
<el-card shadow="hover" body-style="padding: 0;">
|
||||||
<div id="fullscreen-prediction-chart-history" style="width: 100%; height: 500px;"></div>
|
<canvas ref="chartCanvas" style="width: 100%; height: 500px;"></canvas>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -122,29 +122,29 @@
|
|||||||
<div class="stat-cards">
|
<div class="stat-cards">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>平均预测销量</span></div></template>
|
<template #header><div class="stat-header"><span>平均预测销量</span></div></template>
|
||||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateAverage(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateAverage(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>最高预测销量</span></div></template>
|
<template #header><div class="stat-header"><span>最高预测销量</span></div></template>
|
||||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMax(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMax(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>最低预测销量</span></div></template>
|
<template #header><div class="stat-header"><span>最低预测销量</span></div></template>
|
||||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMin(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMin(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="currentPrediction?.data?.prediction_data || []" stripe height="330" border>
|
<el-table :data="currentPrediction?.prediction_data || []" stripe height="330" border>
|
||||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="sales" label="预测销量" sortable>
|
<el-table-column prop="sales" label="预测销量" sortable>
|
||||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="趋势">
|
<el-table-column label="趋势">
|
||||||
<template #default="{ row, $index }">
|
<template #default="{ row, $index }">
|
||||||
<div v-if="$index > 0 && currentPrediction?.data?.prediction_data">
|
<div v-if="$index > 0 && currentPrediction?.prediction_data">
|
||||||
<el-icon v-if="row.sales > currentPrediction.data.prediction_data[$index-1].sales" color="#67C23A"><ArrowUp /></el-icon>
|
<el-icon v-if="row.sales > currentPrediction.prediction_data[$index-1].sales" color="#67C23A"><ArrowUp /></el-icon>
|
||||||
<el-icon v-else-if="row.sales < currentPrediction.data.prediction_data[$index-1].sales" color="#F56C6C"><ArrowDown /></el-icon>
|
<el-icon v-else-if="row.sales < currentPrediction.prediction_data[$index-1].sales" color="#F56C6C"><ArrowDown /></el-icon>
|
||||||
<el-icon v-else color="#909399"><Minus /></el-icon>
|
<el-icon v-else color="#909399"><Minus /></el-icon>
|
||||||
{{ Math.abs(row.sales - currentPrediction.data.prediction_data[$index-1].sales).toFixed(2) }}
|
{{ Math.abs(row.sales - currentPrediction.prediction_data[$index-1].sales).toFixed(2) }}
|
||||||
</div>
|
</div>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
@ -152,7 +152,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="历史数据">
|
<el-tab-pane label="历史数据">
|
||||||
<el-table :data="currentPrediction?.data?.history_data || []" stripe height="400" border>
|
<el-table :data="currentPrediction?.history_data || []" stripe height="400" border>
|
||||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="sales" label="历史销量" sortable>
|
<el-table-column prop="sales" label="历史销量" sortable>
|
||||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||||
@ -160,7 +160,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="全部数据">
|
<el-tab-pane label="全部数据">
|
||||||
<el-table :data="currentPrediction?.data?.data || []" stripe height="400" border>
|
<el-table :data="[...(currentPrediction?.history_data || []).map(d => ({...d, data_type: '历史销量'})), ...(currentPrediction?.prediction_data || []).map(d => ({...d, data_type: '预测销量'}))]" stripe height="400" border>
|
||||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="sales" label="销量" sortable>
|
<el-table-column prop="sales" label="销量" sortable>
|
||||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||||
@ -248,7 +248,7 @@ import { ref, onMounted, reactive, watch, nextTick } from 'vue';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { QuestionFilled, Search, View, Delete, ArrowUp, ArrowDown, Minus, Download } from '@element-plus/icons-vue';
|
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';
|
import { computed, onUnmounted } from 'vue';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -257,10 +257,9 @@ const products = ref([]);
|
|||||||
const modelTypes = ref([]);
|
const modelTypes = ref([]);
|
||||||
const detailsVisible = ref(false);
|
const detailsVisible = ref(false);
|
||||||
const currentPrediction = ref(null);
|
const currentPrediction = ref(null);
|
||||||
const rawResponseData = ref(null);
|
const chartCanvas = ref(null); // 新增 ref
|
||||||
const showRawDataFlag = ref(false);
|
|
||||||
|
|
||||||
let predictionChart = null; // << 关键改动:使用单个chart实例
|
let predictionChart = null;
|
||||||
let historyChart = null;
|
let historyChart = null;
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
@ -327,7 +326,6 @@ const handleCurrentChange = (page) => {
|
|||||||
fetchHistory();
|
fetchHistory();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加getProductName函数
|
|
||||||
const getProductName = (productId) => {
|
const getProductName = (productId) => {
|
||||||
if (!productId) return '未知产品';
|
if (!productId) return '未知产品';
|
||||||
const product = products.value.find(p => p.product_id === productId);
|
const product = products.value.find(p => p.product_id === productId);
|
||||||
@ -338,68 +336,11 @@ const viewDetails = async (id) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/prediction/history/${id}`);
|
const response = await axios.get(`/api/prediction/history/${id}`);
|
||||||
const responseData = response.data;
|
const responseData = response.data;
|
||||||
rawResponseData.value = JSON.stringify(responseData, null, 2);
|
|
||||||
|
|
||||||
if (responseData && responseData.status === 'success') {
|
if (responseData && responseData.status === 'success' && responseData.data) {
|
||||||
// 后端已经返回标准格式的数据,直接使用
|
currentPrediction.value = responseData.data;
|
||||||
// 但仍需确保数据结构的完整性
|
|
||||||
|
|
||||||
// 确保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;
|
|
||||||
detailsVisible.value = true;
|
detailsVisible.value = true;
|
||||||
|
console.log('标准化的预测详情数据:', currentPrediction.value);
|
||||||
console.log('预测详情数据:', responseData);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
||||||
}
|
}
|
||||||
@ -425,13 +366,11 @@ const deleteHistory = (id) => {
|
|||||||
try {
|
try {
|
||||||
await axios.delete(`/api/prediction/history/${id}`);
|
await axios.delete(`/api/prediction/history/${id}`);
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
fetchHistory(); // 重新加载数据
|
fetchHistory();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('删除失败');
|
ElMessage.error('删除失败');
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {});
|
||||||
//
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateTime = (isoString) => {
|
const formatDateTime = (isoString) => {
|
||||||
@ -462,400 +401,6 @@ const getModelTagType = (modelType) => {
|
|||||||
return types[modelType] || 'info';
|
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) => {
|
const calculateAverage = (data) => {
|
||||||
if (!data || data.length === 0) return 0;
|
if (!data || data.length === 0) return 0;
|
||||||
return data.reduce((sum, item) => sum + item.sales, 0) / data.length;
|
return data.reduce((sum, item) => sum + item.sales, 0) / data.length;
|
||||||
@ -951,25 +496,21 @@ watch(detailsVisible, (newVal) => {
|
|||||||
if (newVal && currentPrediction.value) {
|
if (newVal && currentPrediction.value) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
renderChart();
|
renderChart();
|
||||||
// 可以在这里添加渲染第二个图表的逻辑
|
|
||||||
// renderHistoryAnalysisChart();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// << 关键改动:从ProductPredictionView.vue复制并适应的renderChart函数
|
// 参照 ProductPredictionView.vue 的实现进行重构
|
||||||
const renderChart = () => {
|
const renderChart = () => {
|
||||||
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
|
if (!chartCanvas.value || !currentPrediction.value) return;
|
||||||
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
|
|
||||||
|
|
||||||
if (predictionChart) {
|
if (predictionChart) {
|
||||||
predictionChart.destroy();
|
predictionChart.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date) => new Date(date).toISOString().split('T')[0];
|
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 historyData = (currentPrediction.value.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||||
const predictionData = (currentPrediction.value.data.prediction_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) {
|
if (historyData.length === 0 && predictionData.length === 0) {
|
||||||
ElMessage.warning('没有可用于图表的数据。');
|
ElMessage.warning('没有可用于图表的数据。');
|
||||||
@ -977,20 +518,24 @@ const renderChart = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
|
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]));
|
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 predictionMap = new Map(predictionData.map(p => [p.date, p.sales]));
|
||||||
|
|
||||||
const alignedHistorySales = allLabels.map(label => historyMap.get(label) ?? null);
|
const alignedHistorySales = allLabels.map(label => historyMap.get(label) ?? null);
|
||||||
const alignedPredictionSales = allLabels.map(label => predictionMap.get(label) ?? null);
|
const alignedPredictionSales = allLabels.map(label => predictionMap.get(label) ?? null);
|
||||||
|
|
||||||
|
// 连接点逻辑
|
||||||
if (historyData.length > 0 && predictionData.length > 0) {
|
if (historyData.length > 0 && predictionData.length > 0) {
|
||||||
const lastHistoryDate = historyData[historyData.length - 1].date;
|
const lastHistoryDate = historyData[historyData.length - 1].date;
|
||||||
const lastHistoryValue = historyData[historyData.length - 1].sales;
|
const lastHistoryValue = historyData[historyData.length - 1].sales;
|
||||||
if (!predictionMap.has(lastHistoryDate)) {
|
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}`;
|
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
predictionChart = new Chart(chartCanvas, {
|
predictionChart = new Chart(chartCanvas.value, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: simplifiedLabels,
|
labels: simplifiedLabels,
|
||||||
@ -1015,7 +560,6 @@ const renderChart = () => {
|
|||||||
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: true,
|
fill: true,
|
||||||
spanGaps: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '预测销量',
|
label: '预测销量',
|
||||||
@ -1032,10 +576,15 @@ const renderChart = () => {
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`,
|
text: `${currentPrediction.value.product_name || '未知产品'} - 销量预测趋势图`,
|
||||||
color: '#ffffff',
|
color: 'white',
|
||||||
font: {
|
font: {
|
||||||
size: 20,
|
size: 20,
|
||||||
weight: 'bold',
|
weight: 'bold',
|
||||||
@ -1044,7 +593,7 @@ const renderChart = () => {
|
|||||||
subtitle: {
|
subtitle: {
|
||||||
display: true,
|
display: true,
|
||||||
text: subtitleText,
|
text: subtitleText,
|
||||||
color: '#6c757d',
|
color: 'white',
|
||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
},
|
},
|
||||||
@ -1057,7 +606,11 @@ const renderChart = () => {
|
|||||||
x: {
|
x: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: '日期 (日)'
|
text: '日期',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'white'
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: false
|
display: false
|
||||||
@ -1066,10 +619,14 @@ const renderChart = () => {
|
|||||||
y: {
|
y: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: '销量'
|
text: '销量',
|
||||||
|
color: 'white'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'white'
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: '#e9e9e9',
|
color: 'rgba(255, 255, 255, 0.2)',
|
||||||
drawBorder: false,
|
drawBorder: false,
|
||||||
},
|
},
|
||||||
beginAtZero: true
|
beginAtZero: true
|
||||||
@ -1081,7 +638,7 @@ const renderChart = () => {
|
|||||||
|
|
||||||
const exportHistoryData = () => {
|
const exportHistoryData = () => {
|
||||||
if (!currentPrediction.value) return;
|
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";
|
let csvContent = "日期,销量,数据类型\n";
|
||||||
data.forEach(row => {
|
data.forEach(row => {
|
||||||
csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`;
|
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 url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
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`);
|
link.setAttribute('download', `历史预测_${product_name}_${model_type}_${start_date}.csv`);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
|
123
fix_old_predictions.py
Normal file
123
fix_old_predictions.py
Normal file
@ -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()
|
Binary file not shown.
@ -1370,10 +1370,10 @@ def predict():
|
|||||||
data = request.json
|
data = request.json
|
||||||
model_type = data.get('model_type')
|
model_type = data.get('model_type')
|
||||||
version = data.get('version')
|
version = data.get('version')
|
||||||
future_days = int(data.get('future_days', 7))
|
future_days = int(data['future_days'])
|
||||||
start_date = data.get('start_date', '')
|
start_date = data.get('start_date', '')
|
||||||
include_visualization = data.get('include_visualization', False)
|
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')
|
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)
|
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类型
|
# 处理预测结果中可能存在的NumPy类型
|
||||||
def convert_numpy_types(obj):
|
def convert_numpy_types(obj):
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user