diff --git a/UI/src/views/HistoryView.vue b/UI/src/views/HistoryView.vue
index 80d5f5d..ec19ed3 100644
--- a/UI/src/views/HistoryView.vue
+++ b/UI/src/views/HistoryView.vue
@@ -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 = `
${date}
`;
- params.forEach(item => {
- if (item.value !== '-') {
- html += `
-
- ${item.seriesName}:
- ${item.value.toFixed(2)}
-
`;
- }
- });
- 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}
环比: ${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 = () => {
diff --git a/UI/src/views/prediction/GlobalPredictionView.vue b/UI/src/views/prediction/GlobalPredictionView.vue
index 10fab64..98e5b08 100644
--- a/UI/src/views/prediction/GlobalPredictionView.vue
+++ b/UI/src/views/prediction/GlobalPredictionView.vue
@@ -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: {
diff --git a/UI/src/views/prediction/ProductPredictionView.vue b/UI/src/views/prediction/ProductPredictionView.vue
index 7e43af4..279d38f 100644
--- a/UI/src/views/prediction/ProductPredictionView.vue
+++ b/UI/src/views/prediction/ProductPredictionView.vue
@@ -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: {
diff --git a/UI/src/views/prediction/StorePredictionView.vue b/UI/src/views/prediction/StorePredictionView.vue
index 828aba2..1eb0c1d 100644
--- a/UI/src/views/prediction/StorePredictionView.vue
+++ b/UI/src/views/prediction/StorePredictionView.vue
@@ -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: {
diff --git a/prediction_history.db b/prediction_history.db
index 31e42ae..4b9b54e 100644
Binary files a/prediction_history.db and b/prediction_history.db differ
diff --git a/server/api.py b/server/api.py
index fb27a43..aba5aee 100644
--- a/server/api.py
+++ b/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/', 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
diff --git a/server/predictors/model_predictor.py b/server/predictors/model_predictor.py
index e5766b3..1f57caf 100644
--- a/server/predictors/model_predictor.py
+++ b/server/predictors/model_predictor.py
@@ -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 = "全局销售数据"