diff --git a/UI/src/views/HistoryView.vue b/UI/src/views/HistoryView.vue index b223167..2cc9131 100644 --- a/UI/src/views/HistoryView.vue +++ b/UI/src/views/HistoryView.vue @@ -18,7 +18,7 @@ - + { } }; -const fetchProducts = async () => { - try { - const response = await axios.get('/api/products'); - if (response.data.status === 'success') { - products.value = response.data.data; - } - } catch (error) { - ElMessage.error('获取产品列表失败'); - } -}; +// fetchProducts 函数不再需要,将从历史记录中动态生成产品列表 const fetchHistory = async () => { loading.value = true; @@ -309,6 +300,17 @@ const fetchHistory = async () => { if (response.data.status === 'success') { history.value = response.data.data; pagination.total = response.data.total; + + // 从返回的历史记录中动态提取唯一的产品列表 + if (response.data.data && response.data.data.length > 0) { + const uniqueProducts = new Map(); + response.data.data.forEach(record => { + if (record.product_id && record.product_name) { + uniqueProducts.set(record.product_id, record.product_name); + } + }); + products.value = Array.from(uniqueProducts, ([product_id, product_name]) => ({ product_id, product_name })); + } } } catch (error) { ElMessage.error('获取历史记录失败'); @@ -1120,7 +1122,7 @@ onUnmounted(() => { }); onMounted(() => { - fetchProducts(); + // fetchProducts(); // 不再需要独立获取产品列表 fetchModelTypes(); fetchHistory(); }); diff --git a/UI/src/views/prediction/GlobalPredictionView.vue b/UI/src/views/prediction/GlobalPredictionView.vue index abd4fcf..24824cc 100644 --- a/UI/src/views/prediction/GlobalPredictionView.vue +++ b/UI/src/views/prediction/GlobalPredictionView.vue @@ -54,7 +54,7 @@ type="primary" size="small" @click="startPrediction(row)" - :loading="predicting[row.model_id]" + :loading="predicting[row.model_uid]" > 开始预测 @@ -164,33 +164,31 @@ const fetchModels = async () => { } const startPrediction = async (model) => { - predicting[model.model_id] = true + predicting[model.model_uid] = true; // 使用 model_uid 作为唯一的键 try { const payload = { - training_mode: 'global', - model_type: model.model_type, - version: model.version, + model_uid: model.model_uid, // 关键修复:使用 model_uid future_days: form.future_days, history_lookback_days: form.history_lookback_days, start_date: form.start_date, - analyze_result: form.analyze_result, - } - const response = await axios.post('/api/prediction', payload) + include_visualization: true, + }; + const response = await axios.post('/api/prediction', payload); if (response.data.status === 'success') { - predictionResult.value = response.data.data - ElMessage.success('预测完成!') - dialogVisible.value = true - await nextTick() - renderChart() + predictionResult.value = response.data.data; + ElMessage.success('预测完成!'); + dialogVisible.value = true; + await nextTick(); + renderChart(); } else { - ElMessage.error(response.data.error || '预测失败') + ElMessage.error(response.data.error || '预测失败'); } } catch (error) { - ElMessage.error(error.response?.data?.error || '预测请求失败') + ElMessage.error(error.response?.data?.error || '预测请求失败'); } finally { - predicting[model.model_id] = false + predicting[model.model_uid] = false; // 保持键的一致性 } -} +}; const renderChart = () => { if (!chartCanvas.value || !predictionResult.value) return diff --git a/UI/src/views/prediction/ProductPredictionView.vue b/UI/src/views/prediction/ProductPredictionView.vue index 47a76a0..451ae25 100644 --- a/UI/src/views/prediction/ProductPredictionView.vue +++ b/UI/src/views/prediction/ProductPredictionView.vue @@ -63,7 +63,7 @@ type="primary" size="small" @click="startPrediction(row)" - :loading="predicting[row.model_id]" + :loading="predicting[row.model_uid]" > 开始预测 @@ -174,16 +174,14 @@ const fetchModels = async () => { } const startPrediction = async (model) => { - predicting[model.model_id] = true + predicting[model.model_uid] = true // 使用 model_uid 作为唯一的键 try { const payload = { - product_id: model.product_id, - model_type: model.model_type, - version: model.version, + model_uid: model.model_uid, // 关键修复:使用 model_uid future_days: form.future_days, history_lookback_days: form.history_lookback_days, start_date: form.start_date, - include_visualization: true, // 分析功能硬编码为开启 + include_visualization: true, } const response = await axios.post('/api/prediction', payload) if (response.data.status === 'success') { @@ -198,7 +196,7 @@ const startPrediction = async (model) => { } catch (error) { ElMessage.error(error.response?.data?.error || '预测请求失败') } finally { - predicting[model.model_id] = false + predicting[model.model_uid] = false } } diff --git a/UI/src/views/prediction/StorePredictionView.vue b/UI/src/views/prediction/StorePredictionView.vue index 88f6b93..e17179c 100644 --- a/UI/src/views/prediction/StorePredictionView.vue +++ b/UI/src/views/prediction/StorePredictionView.vue @@ -63,7 +63,7 @@ type="primary" size="small" @click="startPrediction(row)" - :loading="predicting[row.model_id]" + :loading="predicting[row.model_uid]" > 开始预测 @@ -187,34 +187,31 @@ const fetchStores = async () => { } const startPrediction = async (model) => { - predicting[model.model_id] = true + predicting[model.model_uid] = true; // 使用 model_uid 作为唯一的键 try { const payload = { - training_mode: 'store', - store_id: model.store_id, - model_type: model.model_type, - version: model.version, + model_uid: model.model_uid, // 关键修复:使用 model_uid future_days: form.future_days, history_lookback_days: form.history_lookback_days, start_date: form.start_date, - analyze_result: form.analyze_result, - } - const response = await axios.post('/api/prediction', payload) + include_visualization: true, + }; + const response = await axios.post('/api/prediction', payload); if (response.data.status === 'success') { - predictionResult.value = response.data.data - ElMessage.success('预测完成!') - dialogVisible.value = true - await nextTick() - renderChart() + predictionResult.value = response.data.data; + ElMessage.success('预测完成!'); + dialogVisible.value = true; + await nextTick(); + renderChart(); } else { - ElMessage.error(response.data.error || '预测失败') + ElMessage.error(response.data.error || '预测失败'); } } catch (error) { - ElMessage.error(error.response?.data?.error || '预测请求失败') + ElMessage.error(error.response?.data?.error || '预测请求失败'); } finally { - predicting[model.model_id] = false + predicting[model.model_uid] = false; // 保持键的一致性 } -} +}; const renderChart = () => { if (!chartCanvas.value || !predictionResult.value) return diff --git a/prediction_history.db b/prediction_history.db index 75a2a12..167ec84 100644 Binary files a/prediction_history.db and b/prediction_history.db differ diff --git a/server/api.py b/server/api.py index 130781b..e0755b1 100644 --- a/server/api.py +++ b/server/api.py @@ -1383,7 +1383,7 @@ def predict(): "prediction_uid": prediction_uid, "model_id": model_uid, "model_type": model_type, - "product_name": model_record.get('display_name'), + "product_name": prediction_result.get('product_name') or model_record.get('display_name'), "prediction_scope": {"product_id": product_id, "store_id": store_id}, "prediction_params": {"future_days": future_days, "start_date": start_date}, "metrics": prediction_result.get('analysis', {}).get('metrics', {}), @@ -1672,13 +1672,18 @@ def get_prediction_history(): # 连接数据库 conn = get_db_connection() cursor = conn.cursor() + + # 获取产品ID到名称的映射,用于修正历史数据中的产品名称 + from utils.multi_store_data_utils import get_available_products + all_products = get_available_products() + product_name_map = {p['product_id']: p['product_name'] for p in all_products} # 构建查询条件 query_conditions = [] query_params = [] if product_id: - query_conditions.append("product_id = ?") + query_conditions.append("json_extract(prediction_scope, '$.product_id') = ?") query_params.append(product_id) if model_type: @@ -1709,32 +1714,40 @@ 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}") + # 解析JSON字段 + prediction_scope = json.loads(record['prediction_scope']) if record['prediction_scope'] else {} + prediction_params = json.loads(record['prediction_params']) if record['prediction_params'] else {} - history_records.append({ - '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'] - }) + # 安全地获取嵌套值 + product_id = prediction_scope.get('product_id', 'N/A') + start_date_str = prediction_params.get('start_date', 'N/A') + future_days = prediction_params.get('future_days', 'N/A') + + created_at_str = record['created_at'] + formatted_created_at = created_at_str + + try: + dt_obj = datetime.fromisoformat(created_at_str) + 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['id'], + 'prediction_uid': record['prediction_uid'], + 'product_id': product_id, + 'product_name': product_name_map.get(product_id, record['product_name']), + 'model_type': record['model_type'], + 'model_id': record['model_id'], + 'start_date': start_date_str, + 'future_days': future_days, + 'created_at': formatted_created_at, + 'file_path': record['result_file_path'] + }) + except (json.JSONDecodeError, KeyError) as e: + logger.error(f"处理历史记录失败 (ID: {record['id']}): {e}") + continue conn.close() diff --git a/xz数据库2025_07_24.md b/xz数据库2025_07_24.md index 218a29f..296e8fb 100644 --- a/xz数据库2025_07_24.md +++ b/xz数据库2025_07_24.md @@ -11,6 +11,7 @@ - **结构清晰**: 物理文件(模型、日志、预测结果)与数据库记录分离,数据库只存元数据和路径,保持自身轻量。 --- +## 预测结果JSON保存到:saved_predictions ## 表结构定义