完善历史预测展示
This commit is contained in:
parent
e4d170d667
commit
244393670d
@ -248,41 +248,9 @@ 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 * as echarts from 'echarts/core';
|
||||
import { LineChart, BarChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
MarkLineComponent,
|
||||
MarkPointComponent
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import Chart from 'chart.js/auto'; // << 关键改动:导入Chart.js
|
||||
import { computed, onUnmounted } from 'vue';
|
||||
|
||||
// 注册必须的组件
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
MarkLineComponent,
|
||||
MarkPointComponent,
|
||||
LineChart,
|
||||
BarChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const loading = ref(false);
|
||||
const history = ref([]);
|
||||
const products = ref([]);
|
||||
@ -292,8 +260,8 @@ const currentPrediction = ref(null);
|
||||
const rawResponseData = ref(null);
|
||||
const showRawDataFlag = ref(false);
|
||||
|
||||
const fullscreenPredictionChart = ref(null);
|
||||
const fullscreenHistoryChart = ref(null);
|
||||
let predictionChart = null; // << 关键改动:使用单个chart实例
|
||||
let historyChart = null;
|
||||
|
||||
const filters = reactive({
|
||||
product_id: '',
|
||||
@ -982,104 +950,133 @@ const getFactorsArray = computed(() => {
|
||||
watch(detailsVisible, (newVal) => {
|
||||
if (newVal && currentPrediction.value) {
|
||||
nextTick(() => {
|
||||
// Init Prediction Chart
|
||||
if (fullscreenPredictionChart.value) fullscreenPredictionChart.value.dispose();
|
||||
const predChartDom = document.getElementById('fullscreen-prediction-chart-history');
|
||||
if (predChartDom) {
|
||||
fullscreenPredictionChart.value = echarts.init(predChartDom);
|
||||
if (currentPrediction.value.chart_data) {
|
||||
updatePredictionChart(currentPrediction.value.chart_data, fullscreenPredictionChart.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Init History Chart
|
||||
if (currentPrediction.value.analysis) {
|
||||
if (fullscreenHistoryChart.value) fullscreenHistoryChart.value.dispose();
|
||||
const histChartDom = document.getElementById('fullscreen-history-chart-history');
|
||||
if (histChartDom) {
|
||||
fullscreenHistoryChart.value = echarts.init(histChartDom);
|
||||
updateHistoryChart(currentPrediction.value.analysis, fullscreenHistoryChart.value, true);
|
||||
}
|
||||
}
|
||||
renderChart();
|
||||
// 可以在这里添加渲染第二个图表的逻辑
|
||||
// renderHistoryAnalysisChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const updatePredictionChart = (chartData, chart, isFullscreen = false) => {
|
||||
if (!chart || !chartData) return;
|
||||
chart.showLoading();
|
||||
const dates = chartData.dates || [];
|
||||
const sales = chartData.sales || [];
|
||||
const types = chartData.types || [];
|
||||
// << 关键改动:从ProductPredictionView.vue复制并适应的renderChart函数
|
||||
const renderChart = () => {
|
||||
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
|
||||
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
|
||||
|
||||
const combinedData = [];
|
||||
for (let i = 0; i < dates.length; i++) {
|
||||
combinedData.push({ date: dates[i], sales: sales[i], type: types[i] });
|
||||
if (predictionChart) {
|
||||
predictionChart.destroy();
|
||||
}
|
||||
combinedData.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
|
||||
const allDates = combinedData.map(item => item.date);
|
||||
const historyDates = combinedData.filter(d => d.type === '历史销量').map(d => d.date);
|
||||
const historySales = combinedData.filter(d => d.type === '历史销量').map(d => d.sales);
|
||||
const predictionDates = combinedData.filter(d => d.type === '预测销量').map(d => d.date);
|
||||
const predictionSales = combinedData.filter(d => d.type === '预测销量').map(d => d.sales);
|
||||
|
||||
const allSales = [...historySales, ...predictionSales].filter(val => !isNaN(val));
|
||||
const minSale = Math.max(0, Math.floor(Math.min(...allSales) * 0.9));
|
||||
const maxSale = Math.ceil(Math.max(...allSales) * 1.1);
|
||||
|
||||
const option = {
|
||||
title: { text: '销量预测趋势图', left: 'center', textStyle: { fontSize: isFullscreen ? 18 : 16, fontWeight: 'bold', color: '#e0e6ff' } },
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' },
|
||||
formatter: function(params) {
|
||||
if (!params || params.length === 0) return '';
|
||||
const date = params[0].axisValue;
|
||||
let html = `<div style="font-weight:bold">${date}</div>`;
|
||||
params.forEach(item => {
|
||||
if (item.value !== '-') {
|
||||
html += `<div style="display:flex;justify-content:space-between;align-items:center;margin:5px 0;">
|
||||
<span style="display:inline-block;margin-right:5px;width:10px;height:10px;border-radius:50%;background-color:${item.color};"></span>
|
||||
<span>${item.seriesName}:</span>
|
||||
<span style="font-weight:bold;margin-left:5px;">${item.value.toFixed(2)}</span>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
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) }));
|
||||
|
||||
if (historyData.length === 0 && predictionData.length === 0) {
|
||||
ElMessage.warning('没有可用于图表的数据。');
|
||||
return;
|
||||
}
|
||||
|
||||
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
|
||||
const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
|
||||
|
||||
const historyMap = new Map(historyData.map(p => [p.date, p.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;
|
||||
}
|
||||
}
|
||||
|
||||
let subtitleText = '';
|
||||
if (historyData.length > 0) {
|
||||
subtitleText += `历史数据: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
|
||||
}
|
||||
if (predictionData.length > 0) {
|
||||
if (subtitleText) subtitleText += ' | ';
|
||||
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
|
||||
}
|
||||
|
||||
predictionChart = new Chart(chartCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: simplifiedLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: '历史销量',
|
||||
data: alignedHistorySales,
|
||||
borderColor: '#67C23A',
|
||||
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
spanGaps: false,
|
||||
},
|
||||
{
|
||||
label: '预测销量',
|
||||
data: alignedPredictionSales,
|
||||
borderColor: '#409EFF',
|
||||
backgroundColor: 'rgba(64, 158, 255, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
borderDash: [5, 5],
|
||||
}
|
||||
]
|
||||
},
|
||||
legend: { data: ['历史销量', '预测销量'], top: isFullscreen ? 40 : 30, textStyle: { color: '#e0e6ff' } },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
toolbox: { feature: { saveAsImage: { title: '保存图片' } }, iconStyle: { borderColor: '#e0e6ff' } },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: allDates, axisLabel: { color: '#e0e6ff' }, axisLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.5)' } } },
|
||||
yAxis: { type: 'value', name: '销量', min: minSale, max: maxSale, axisLabel: { color: '#e0e6ff' }, nameTextStyle: { color: '#e0e6ff' }, axisLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.1)' } } },
|
||||
series: [
|
||||
{ name: '历史销量', type: 'line', smooth: true, connectNulls: true, data: allDates.map(date => historyDates.includes(date) ? historySales[historyDates.indexOf(date)] : null), areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' }, { offset: 1, color: 'rgba(64, 158, 255, 0.1)' }]) }, lineStyle: { color: '#409EFF' } },
|
||||
{ name: '预测销量', type: 'line', smooth: true, connectNulls: true, data: allDates.map(date => predictionDates.includes(date) ? predictionSales[predictionDates.indexOf(date)] : null), lineStyle: { color: '#F56C6C' } }
|
||||
]
|
||||
};
|
||||
chart.hideLoading();
|
||||
chart.setOption(option, true);
|
||||
};
|
||||
|
||||
const updateHistoryChart = (analysisData, chart, isFullscreen = false) => {
|
||||
if (!chart || !analysisData || !analysisData.history_chart_data) return;
|
||||
chart.showLoading();
|
||||
const { dates, changes } = analysisData.history_chart_data;
|
||||
|
||||
const option = {
|
||||
title: { text: '销量日环比变化', left: 'center', textStyle: { fontSize: isFullscreen ? 18 : 16, fontWeight: 'bold', color: '#e0e6ff' } },
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: p => `${p[0].axisValue}<br/>环比: ${p[0].value.toFixed(2)}%` },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
toolbox: { feature: { saveAsImage: { title: '保存图片' } }, iconStyle: { borderColor: '#e0e6ff' } },
|
||||
xAxis: { type: 'category', data: dates.map(d => formatDate(d)), axisLabel: { color: '#e0e6ff' }, axisLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.5)' } } },
|
||||
yAxis: { type: 'value', name: '环比变化(%)', axisLabel: { formatter: '{value}%', color: '#e0e6ff' }, nameTextStyle: { color: '#e0e6ff' }, axisLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.5)' } }, splitLine: { lineStyle: { color: 'rgba(224, 230, 255, 0.1)' } } },
|
||||
series: [{
|
||||
name: '日环比变化', type: 'bar',
|
||||
data: changes.map(val => ({ value: val, itemStyle: { color: val >= 0 ? '#67C23A' : '#F56C6C' } }))
|
||||
}]
|
||||
};
|
||||
chart.hideLoading();
|
||||
chart.setOption(option, true);
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`,
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: 20,
|
||||
weight: 'bold',
|
||||
}
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: subtitleText,
|
||||
color: '#6c757d',
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
padding: {
|
||||
bottom: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '日期 (日)'
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '销量'
|
||||
},
|
||||
grid: {
|
||||
color: '#e9e9e9',
|
||||
drawBorder: false,
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportHistoryData = () => {
|
||||
|
@ -294,15 +294,22 @@ const renderChart = () => {
|
||||
title: {
|
||||
display: true,
|
||||
text: '全局销量预测趋势图',
|
||||
font: { size: 18 }
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: 20,
|
||||
weight: 'bold',
|
||||
}
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: subtitleText,
|
||||
color: '#6c757d',
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
padding: {
|
||||
bottom: 20
|
||||
},
|
||||
font: { size: 14 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
|
@ -314,16 +314,23 @@ const renderChart = () => {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `“${form.product_id}” - 销量预测趋势图`,
|
||||
font: { size: 18 }
|
||||
text: `${predictionResult.value.product_name} - 销量预测趋势图`,
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: 20,
|
||||
weight: 'bold',
|
||||
}
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: subtitleText,
|
||||
color: '#6c757d',
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
padding: {
|
||||
bottom: 20
|
||||
},
|
||||
font: { size: 14 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
|
@ -312,16 +312,23 @@ const renderChart = () => {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `“店铺${form.store_id}” - 销量预测趋势图`,
|
||||
font: { size: 18 }
|
||||
text: `${predictionResult.value.product_name} - 销量预测趋势图`,
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: 20,
|
||||
weight: 'bold',
|
||||
}
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: subtitleText,
|
||||
color: '#6c757d',
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
padding: {
|
||||
bottom: 20
|
||||
},
|
||||
font: { size: 14 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
|
Binary file not shown.
199
server/api.py
199
server/api.py
@ -1456,6 +1456,27 @@ def predict():
|
||||
print(f"prediction_data 长度: {len(response_data['prediction_data'])}")
|
||||
print("================================")
|
||||
|
||||
# 重新加入保存预测结果的逻辑
|
||||
try:
|
||||
model_id_to_save = f"{model_identifier}_{model_type}_{version}"
|
||||
product_name_to_save = prediction_result.get('product_name', product_id or store_id or 'global')
|
||||
|
||||
# 调用辅助函数保存结果
|
||||
save_prediction_result(
|
||||
prediction_result=prediction_result,
|
||||
product_id=product_id or store_id or 'global',
|
||||
product_name=product_name_to_save,
|
||||
model_type=model_type,
|
||||
model_id=model_id_to_save,
|
||||
start_date=start_date,
|
||||
future_days=future_days
|
||||
)
|
||||
print(f"✅ 预测结果已成功保存到历史记录。")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 警告: 保存预测结果到历史记录失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
# 不应阻止向用户返回结果,因此只打印警告
|
||||
|
||||
return jsonify(response_data)
|
||||
except Exception as e:
|
||||
print(f"预测失败: {str(e)}")
|
||||
@ -1772,16 +1793,31 @@ def get_prediction_history():
|
||||
# 转换结果为字典列表
|
||||
history_records = []
|
||||
for record in records:
|
||||
# 使用列名访问,更安全可靠
|
||||
created_at_str = record['created_at']
|
||||
start_date_str = record['start_date']
|
||||
formatted_created_at = created_at_str # 默认值
|
||||
|
||||
try:
|
||||
# 解析ISO 8601格式的日期时间
|
||||
dt_obj = datetime.fromisoformat(created_at_str)
|
||||
# 格式化为前端期望的 'YYYY/MM/DD HH:MM:SS'
|
||||
formatted_created_at = dt_obj.strftime('%Y/%m/%d %H:%M:%S')
|
||||
except (ValueError, TypeError):
|
||||
# 如果解析失败,记录日志并使用原始字符串
|
||||
logger.warning(f"无法解析历史记录中的日期格式: {created_at_str}")
|
||||
|
||||
history_records.append({
|
||||
'id': record[0],
|
||||
'product_id': record[1],
|
||||
'product_name': record[2],
|
||||
'model_type': record[3],
|
||||
'model_id': record[4],
|
||||
'start_date': record[5],
|
||||
'future_days': record[6],
|
||||
'created_at': record[7],
|
||||
'file_path': record[8]
|
||||
'id': record['id'],
|
||||
'prediction_id': record['prediction_id'],
|
||||
'product_id': record['product_id'],
|
||||
'product_name': record['product_name'],
|
||||
'model_type': record['model_type'],
|
||||
'model_id': record['model_id'],
|
||||
'start_date': start_date_str if start_date_str else "N/A",
|
||||
'future_days': record['future_days'],
|
||||
'created_at': formatted_created_at,
|
||||
'file_path': record['file_path']
|
||||
})
|
||||
|
||||
conn.close()
|
||||
@ -1801,109 +1837,88 @@ def get_prediction_history():
|
||||
|
||||
@app.route('/api/prediction/history/<prediction_id>', methods=['GET'])
|
||||
def get_prediction_details(prediction_id):
|
||||
"""获取特定预测记录的详情"""
|
||||
"""获取特定预测记录的详情 (v7 - 统一前端逻辑后的最终版)"""
|
||||
try:
|
||||
print(f"正在获取预测记录详情,ID: {prediction_id}")
|
||||
logger.info(f"正在获取预测记录详情,ID: {prediction_id}")
|
||||
|
||||
# 连接数据库
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 查询预测记录元数据
|
||||
cursor.execute("""
|
||||
SELECT product_id, product_name, model_type, model_id,
|
||||
start_date, future_days, created_at, file_path
|
||||
FROM prediction_history WHERE id = ?
|
||||
""", (prediction_id,))
|
||||
cursor.execute("SELECT * FROM prediction_history WHERE id = ?", (prediction_id,))
|
||||
record = cursor.fetchone()
|
||||
|
||||
if not record:
|
||||
print(f"预测记录不存在: {prediction_id}")
|
||||
conn.close()
|
||||
return jsonify({"status": "error", "message": "预测记录不存在"}), 404
|
||||
|
||||
# 提取元数据
|
||||
product_id = record['product_id']
|
||||
product_name = record['product_name']
|
||||
model_type = record['model_type']
|
||||
model_id = record['model_id']
|
||||
start_date = record['start_date']
|
||||
future_days = record['future_days']
|
||||
created_at = record['created_at']
|
||||
file_path = record['file_path']
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"正在读取预测结果文件: {file_path}")
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"预测结果文件不存在: {file_path}")
|
||||
if not record:
|
||||
logger.warning(f"数据库中未找到预测记录: ID={prediction_id}")
|
||||
return jsonify({"status": "error", "message": "预测记录不存在"}), 404
|
||||
|
||||
file_path = record['file_path']
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
logger.error(f"预测结果文件不存在或路径为空: {file_path}")
|
||||
return jsonify({"status": "error", "message": "预测结果文件不存在"}), 404
|
||||
|
||||
# 读取保存的JSON文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
prediction_data = json.load(f)
|
||||
saved_data = json.load(f)
|
||||
|
||||
# 构建与预测分析接口一致的响应格式
|
||||
response_data = {
|
||||
"status": "success",
|
||||
"meta": {
|
||||
"product_id": product_id,
|
||||
"product_name": product_name,
|
||||
"model_type": model_type,
|
||||
"model_id": model_id,
|
||||
"start_date": start_date,
|
||||
"future_days": future_days,
|
||||
"created_at": created_at
|
||||
},
|
||||
"data": {
|
||||
"prediction_data": [],
|
||||
"history_data": [],
|
||||
"data": []
|
||||
},
|
||||
"analysis": prediction_data.get('analysis', {}),
|
||||
"chart_data": prediction_data.get('chart_data', {})
|
||||
core_data = saved_data
|
||||
if 'data' in saved_data and isinstance(saved_data.get('data'), dict):
|
||||
nested_data = saved_data['data']
|
||||
if 'history_data' in nested_data or 'prediction_data' in nested_data:
|
||||
core_data = nested_data
|
||||
|
||||
# 1. 数据清洗和字段名统一
|
||||
history_data = core_data.get('history_data', [])
|
||||
prediction_data = core_data.get('prediction_data', [])
|
||||
|
||||
cleaned_history = []
|
||||
for item in (history_data or []):
|
||||
if not isinstance(item, dict): continue
|
||||
sales_val = item.get('sales')
|
||||
cleaned_history.append({
|
||||
'date': item.get('date'),
|
||||
'sales': float(sales_val) if sales_val is not None and not np.isnan(sales_val) else None
|
||||
})
|
||||
|
||||
cleaned_prediction = []
|
||||
for item in (prediction_data or []):
|
||||
if not isinstance(item, dict): continue
|
||||
# 关键修复:将 'predicted_sales' 统一为 'sales'
|
||||
sales_val = item.get('predicted_sales', item.get('sales'))
|
||||
cleaned_prediction.append({
|
||||
'date': item.get('date'),
|
||||
'sales': float(sales_val) if sales_val is not None and not np.isnan(sales_val) else None,
|
||||
# 统一前端逻辑后,不再需要predicted_sales,但为兼容旧数据保留
|
||||
'predicted_sales': float(sales_val) if sales_val is not None and not np.isnan(sales_val) else None
|
||||
})
|
||||
|
||||
# 2. 构建与前端统一逻辑完全兼容的payload
|
||||
final_payload = {
|
||||
'product_name': record['product_name'],
|
||||
'model_type': record['model_type'],
|
||||
'start_date': record['start_date'],
|
||||
'created_at': record['created_at'],
|
||||
'history_data': cleaned_history,
|
||||
'prediction_data': cleaned_prediction,
|
||||
'analysis': core_data.get('analysis', {}),
|
||||
}
|
||||
|
||||
# 处理预测数据
|
||||
if 'prediction_data' in prediction_data and isinstance(prediction_data['prediction_data'], list):
|
||||
response_data['data']['prediction_data'] = prediction_data['prediction_data']
|
||||
# 3. 最终封装
|
||||
response_data = {
|
||||
"status": "success",
|
||||
"data": final_payload
|
||||
}
|
||||
|
||||
logger.info(f"成功构建并返回历史预测详情 (v7): ID={prediction_id}, "
|
||||
f"历史数据点: {len(final_payload['history_data'])}, "
|
||||
f"预测数据点: {len(final_payload['prediction_data'])}")
|
||||
|
||||
# 处理历史数据
|
||||
if 'history_data' in prediction_data and isinstance(prediction_data['history_data'], list):
|
||||
response_data['data']['history_data'] = prediction_data['history_data']
|
||||
|
||||
# 处理合并的数据
|
||||
if 'data' in prediction_data and isinstance(prediction_data['data'], list):
|
||||
response_data['data']['data'] = prediction_data['data']
|
||||
else:
|
||||
# 如果没有合并数据,从历史和预测数据中构建
|
||||
history_data = response_data['data']['history_data']
|
||||
pred_data = response_data['data']['prediction_data']
|
||||
response_data['data']['data'] = history_data + pred_data
|
||||
|
||||
# 确保所有数据字段都存在且格式正确
|
||||
for key in ['prediction_data', 'history_data', 'data']:
|
||||
if not isinstance(response_data['data'][key], list):
|
||||
response_data['data'][key] = []
|
||||
|
||||
# 添加兼容性字段(直接在根级别)
|
||||
response_data.update({
|
||||
'product_id': product_id,
|
||||
'product_name': product_name,
|
||||
'model_type': model_type,
|
||||
'start_date': start_date,
|
||||
'created_at': created_at
|
||||
})
|
||||
|
||||
print(f"成功获取预测详情,产品: {product_name}, 模型: {model_type}")
|
||||
return jsonify(response_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"预测结果文件JSON解析错误: {e}")
|
||||
logger.error(f"预测结果文件JSON解析错误: {file_path}, 错误: {e}")
|
||||
return jsonify({"status": "error", "message": f"预测结果文件格式错误: {str(e)}"}), 500
|
||||
except Exception as e:
|
||||
print(f"获取预测详情失败: {str(e)}")
|
||||
logger.error(f"获取预测详情失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
|
@ -45,8 +45,13 @@ def load_model_and_predict(model_path: str, product_id: str, model_type: str, st
|
||||
# 加载销售数据
|
||||
from utils.multi_store_data_utils import aggregate_multi_store_data
|
||||
if training_mode == 'store' and store_id:
|
||||
# 先从原始数据加载一次以获取店铺名称,聚合会丢失此信息
|
||||
from utils.multi_store_data_utils import load_multi_store_data
|
||||
store_df_for_name = load_multi_store_data(store_id=store_id)
|
||||
product_name = store_df_for_name['store_name'].iloc[0] if not store_df_for_name.empty else f"店铺 {store_id}"
|
||||
|
||||
# 然后再进行聚合获取用于预测的数据
|
||||
product_df = aggregate_multi_store_data(store_id=store_id, aggregation_method='sum', file_path=DEFAULT_DATA_PATH)
|
||||
product_name = product_df['store_name'].iloc[0] if not product_df.empty else f"店铺{store_id}"
|
||||
elif training_mode == 'global':
|
||||
product_df = aggregate_multi_store_data(aggregation_method='sum', file_path=DEFAULT_DATA_PATH)
|
||||
product_name = "全局销售数据"
|
||||
|
Loading…
x
Reference in New Issue
Block a user