解决历史预测界面数据读取出错,详情还是无法加载

This commit is contained in:
xz2000 2025-07-25 12:43:33 +08:00
parent adb5e0f2b4
commit d5ec662070
9 changed files with 147 additions and 156 deletions

View File

@ -225,10 +225,11 @@ onMounted(() => {
// //
watch(() => props.storeId, () => { watch(() => props.storeId, () => {
if (props.storeId !== null) { if (props.storeId !== null && !props.dataSource) {
fetchProducts() fetchProducts()
} }
}) })
</script> </script>
<style scoped> <style scoped>

View File

@ -38,20 +38,21 @@
<div v-loading="loading"> <div v-loading="loading">
<el-empty v-if="history.length === 0 && !loading" description="暂无历史预测记录"></el-empty> <el-empty v-if="history.length === 0 && !loading" description="暂无历史预测记录"></el-empty>
<el-table v-else :data="history" border stripe style="width: 100%"> <el-table v-else :data="history" border stripe style="width: 100%">
<el-table-column prop="product_name" label="产品名称" min-width="150" /> <el-table-column prop="product_name" label="产品名称" min-width="210" show-overflow-tooltip align="center" />
<el-table-column prop="model_type" label="模型类型" min-width="120"> <el-table-column prop="model_type" label="模型类型" min-width="120" show-overflow-tooltip align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getModelTagType(row.model_type)" size="small">{{ row.model_type }}</el-tag> <el-tag :type="getModelTagType(row.model_type)" size="small">{{ row.model_type }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="start_date" label="预测起始日" min-width="120" /> <el-table-column prop="model_version" label="模型版本" min-width="180" show-overflow-tooltip align="center" />
<el-table-column prop="future_days" label="预测天数" min-width="100" /> <el-table-column prop="start_date" label="预测起始日" min-width="120" show-overflow-tooltip align="center" />
<el-table-column prop="created_at" label="预测时间" min-width="180"> <el-table-column prop="future_days" label="预测天数" min-width="60" show-overflow-tooltip align="center" />
<el-table-column prop="created_at" label="预测时间" min-width="180" show-overflow-tooltip align="center">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDateTime(row.created_at) }} {{ formatDateTime(row.created_at) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" fixed="right"> <el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div style="display: flex; gap: 5px;"> <div style="display: flex; gap: 5px;">
<el-button size="small" type="primary" @click="viewDetails(row.id)"> <el-button size="small" type="primary" @click="viewDetails(row.id)">
@ -135,16 +136,16 @@
</div> </div>
<el-table :data="currentPrediction?.data?.prediction_data || []" stripe height="330" border> <el-table :data="currentPrediction?.data?.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="predicted_sales" label="预测销量" sortable>
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template> <template #default="{ row }">{{ row.predicted_sales ? row.predicted_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?.data?.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.predicted_sales > currentPrediction.data.prediction_data[$index-1].predicted_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.predicted_sales < currentPrediction.data.prediction_data[$index-1].predicted_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.predicted_sales - currentPrediction.data.prediction_data[$index-1].predicted_sales).toFixed(2) }}
</div> </div>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
@ -286,7 +287,17 @@ const fetchModelTypes = async () => {
} }
}; };
// fetchProducts 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('获取产品列表失败');
console.error(error);
}
};
const fetchHistory = async () => { const fetchHistory = async () => {
loading.value = true; loading.value = true;
@ -301,16 +312,6 @@ const fetchHistory = async () => {
history.value = response.data.data; history.value = response.data.data;
pagination.total = response.data.total; 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) { } catch (error) {
ElMessage.error('获取历史记录失败'); ElMessage.error('获取历史记录失败');
@ -342,65 +343,12 @@ const viewDetails = async (id) => {
const responseData = response.data; const responseData = response.data;
rawResponseData.value = JSON.stringify(responseData, null, 2); rawResponseData.value = JSON.stringify(responseData, null, 2);
if (responseData && responseData.status === 'success') { if (responseData && responseData.status === 'success' && responseData.data) {
// 使 // 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('预测详情数据:', responseData); console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
} else { } else {
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误')); ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
@ -860,17 +808,23 @@ const parseJSONWithNaN = (jsonString) => {
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; const validData = data.filter(item => typeof item.predicted_sales === 'number');
if (validData.length === 0) return 0;
return validData.reduce((sum, item) => sum + item.predicted_sales, 0) / validData.length;
}; };
const calculateMax = (data) => { const calculateMax = (data) => {
if (!data || data.length === 0) return 0; if (!data || data.length === 0) return 0;
return Math.max(...data.map(item => item.sales)); const validData = data.filter(item => typeof item.predicted_sales === 'number');
if (validData.length === 0) return 0;
return Math.max(...validData.map(item => item.predicted_sales));
}; };
const calculateMin = (data) => { const calculateMin = (data) => {
if (!data || data.length === 0) return 0; if (!data || data.length === 0) return 0;
return Math.min(...data.map(item => item.sales)); const validData = data.filter(item => typeof item.predicted_sales === 'number');
if (validData.length === 0) return 0;
return Math.min(...validData.map(item => item.predicted_sales));
}; };
const getTrendTagType = (trend) => { const getTrendTagType = (trend) => {
@ -974,7 +928,12 @@ const renderChart = () => {
const predictionData = (currentPrediction.value.data.prediction_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) { if (historyData.length === 0 && predictionData.length === 0) {
ElMessage.warning('没有可用于图表的数据。'); const ctx = chartCanvas.getContext('2d');
ctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height);
ctx.textAlign = 'center';
ctx.fillStyle = '#909399';
ctx.font = '20px Arial';
ctx.fillText('暂无详细图表数据', chartCanvas.width / 2, chartCanvas.height / 2);
return; return;
} }
@ -982,8 +941,8 @@ const renderChart = () => {
const simplifiedLabels = allLabels.map(date => date.split('-')[2]); const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
const historyMap = new Map(historyData.map(p => [p.date, p.sales])); const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
// 使 'sales' // 使 'predicted_sales'
const predictionMap = new Map(predictionData.map(p => [p.date, p.sales])); const predictionMap = new Map(predictionData.map(p => [p.date, p.predicted_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);
@ -1122,7 +1081,7 @@ onUnmounted(() => {
}); });
onMounted(() => { onMounted(() => {
// fetchProducts(); // fetchProducts();
fetchModelTypes(); fetchModelTypes();
fetchHistory(); fetchHistory();
}); });

View File

@ -16,10 +16,10 @@
</template> </template>
<el-form :inline="true" @submit.prevent="fetchModels"> <el-form :inline="true" @submit.prevent="fetchModels">
<el-form-item label="产品ID"> <el-form-item label="产品ID" style="width:300px">
<el-input v-model="filters.product_id" placeholder="按产品ID筛选" clearable></el-input> <el-input v-model="filters.product_id" placeholder="按产品ID筛选" clearable></el-input>
</el-form-item> </el-form-item>
<el-form-item label="模型类型"> <el-form-item label="模型类型" style="width:300px">
<el-select v-model="filters.model_type" placeholder="按模型类型筛选" clearable> <el-select v-model="filters.model_type" placeholder="按模型类型筛选" clearable>
<el-option <el-option
v-for="item in modelTypes" v-for="item in modelTypes"

View File

@ -153,6 +153,7 @@ const fetchModels = async () => {
const response = await axios.get('/api/models', { params: { training_mode: 'global' } }) const response = await axios.get('/api/models', { params: { training_mode: 'global' } })
if (response.data.status === 'success') { if (response.data.status === 'success') {
modelList.value = response.data.data modelList.value = response.data.data
console.log('全局模型列表:', response.data.data)
} else { } else {
ElMessage.error('获取模型列表失败') ElMessage.error('获取模型列表失败')
} }

View File

@ -100,7 +100,6 @@ import axios from 'axios'
import { ElMessage, ElDialog, ElTable, ElTableColumn, ElButton, ElIcon, ElCard, ElTooltip, ElForm, ElFormItem, ElInputNumber, ElDatePicker, ElSelect, ElOption, ElRow, ElCol, ElPagination } from 'element-plus' import { ElMessage, ElDialog, ElTable, ElTableColumn, ElButton, ElIcon, ElCard, ElTooltip, ElForm, ElFormItem, ElInputNumber, ElDatePicker, ElSelect, ElOption, ElRow, ElCol, ElPagination } from 'element-plus'
import { QuestionFilled, TrendCharts } from '@element-plus/icons-vue' import { QuestionFilled, TrendCharts } from '@element-plus/icons-vue'
import Chart from 'chart.js/auto' import Chart from 'chart.js/auto'
import ProductSelector from '../../components/ProductSelector.vue'
const modelList = ref([]) const modelList = ref([])
const modelTypes = ref([]) const modelTypes = ref([])

Binary file not shown.

View File

@ -171,6 +171,8 @@ def init_db():
cursor.execute('ALTER TABLE prediction_history ADD COLUMN prediction_scope TEXT') cursor.execute('ALTER TABLE prediction_history ADD COLUMN prediction_scope TEXT')
if 'result_file_path' not in columns: if 'result_file_path' not in columns:
cursor.execute('ALTER TABLE prediction_history ADD COLUMN result_file_path TEXT') cursor.execute('ALTER TABLE prediction_history ADD COLUMN result_file_path TEXT')
if 'model_version' not in columns:
cursor.execute('ALTER TABLE prediction_history ADD COLUMN model_version TEXT')
# 确保 model_id 字段存在且类型正确 # 确保 model_id 字段存在且类型正确
# 在SQLite中修改列类型比较复杂通常建议重建。此处简化处理。 # 在SQLite中修改列类型比较复杂通常建议重建。此处简化处理。
@ -1340,8 +1342,16 @@ def predict():
# 解析 artifacts 找到模型文件路径 # 解析 artifacts 找到模型文件路径
artifacts = json.loads(model_record.get('artifacts', '{}')) artifacts = json.loads(model_record.get('artifacts', '{}'))
model_file_path = artifacts.get('best_model') or artifacts.get('versioned_model') model_file_path = artifacts.get('best_model') or artifacts.get('versioned_model')
if not model_file_path or not os.path.exists(model_file_path):
return jsonify({"status": "error", "message": "找不到模型文件或文件路径无效"}), 404 # 修正路径问题:将相对路径转换为绝对路径以进行可靠的文件检查
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
absolute_model_path = None
if model_file_path:
absolute_model_path = os.path.join(project_root, model_file_path)
if not absolute_model_path or not os.path.exists(absolute_model_path):
logger.error(f"模型文件检查失败。相对路径: '{model_file_path}', 检查的绝对路径: '{absolute_model_path}'")
return jsonify({"status": "error", "message": f"找不到模型文件或文件路径无效: {model_file_path}"}), 404
# 解析 training_scope 获取 product_id 或 store_id # 解析 training_scope 获取 product_id 或 store_id
training_scope = json.loads(model_record.get('training_scope', '{}')) training_scope = json.loads(model_record.get('training_scope', '{}'))
@ -1356,7 +1366,7 @@ def predict():
# 调用核心预测函数 # 调用核心预测函数
prediction_result = load_model_and_predict( prediction_result = load_model_and_predict(
model_path=model_file_path, model_path=absolute_model_path,
product_id=product_id, product_id=product_id,
model_type=model_type, model_type=model_type,
store_id=store_id, store_id=store_id,
@ -1371,22 +1381,46 @@ def predict():
if prediction_result is None: if prediction_result is None:
return jsonify({"status": "error", "message": "预测失败核心预测器返回None"}), 500 return jsonify({"status": "error", "message": "预测失败核心预测器返回None"}), 500
# 保存预测记录到数据库 # 遵循用户规范,使用相对路径生成文件名
model_display_name = model_record.get('display_name', 'unknown_model')
safe_model_name = secure_filename(model_display_name).replace(' ', '_').replace('-_', '_')
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
prediction_uid = str(uuid.uuid4()) prediction_uid = str(uuid.uuid4())
result_file_path = os.path.join('saved_predictions', f'prediction_{prediction_uid}.json')
os.makedirs(os.path.dirname(result_file_path), exist_ok=True) # 1. 确保保存目录存在
save_dir = 'saved_predictions'
os.makedirs(save_dir, exist_ok=True)
# 2. 遵循用户文档的命名规范,并保持为相对路径
filename = f'{safe_model_name}_pred_{timestamp}.json'
result_file_path = os.path.join(save_dir, filename)
with open(result_file_path, 'w', encoding='utf-8') as f: with open(result_file_path, 'w', encoding='utf-8') as f:
json.dump(prediction_result, f, ensure_ascii=False, cls=CustomJSONEncoder) json.dump(prediction_result, f, ensure_ascii=False, cls=CustomJSONEncoder)
# 确定要显示的名称
product_name_to_save = "N/A"
if training_mode == 'product':
product_name_to_save = training_scope.get('product', {}).get('name') or prediction_result.get('product_name') or f"产品 {product_id}"
elif training_mode == 'store':
product_name_to_save = training_scope.get('store', {}).get('name') or f"店铺 {store_id}"
elif training_mode == 'global':
product_name_to_save = "全局预测"
# 安全地提取分析结果
analysis_result = prediction_result.get('analysis', {}) if prediction_result else {}
metrics_result = analysis_result.get('metrics', {}) if analysis_result else {}
db_payload = { db_payload = {
"prediction_uid": prediction_uid, "prediction_uid": prediction_uid,
"model_id": model_uid, "model_id": model_uid,
"model_type": model_type, "model_type": model_type,
"product_name": prediction_result.get('product_name') or model_record.get('display_name'), "product_name": product_name_to_save, # 使用修正后的名称
"model_version": model_record.get('display_name'), # 将模型信息保存到新字段
"prediction_scope": {"product_id": product_id, "store_id": store_id}, "prediction_scope": {"product_id": product_id, "store_id": store_id},
"prediction_params": {"future_days": future_days, "start_date": start_date}, "prediction_params": {"future_days": future_days, "start_date": start_date},
"metrics": prediction_result.get('analysis', {}).get('metrics', {}), "metrics": metrics_result,
"result_file_path": result_file_path "result_file_path": result_file_path
} }
save_prediction_to_db(db_payload) save_prediction_to_db(db_payload)
@ -1673,10 +1707,6 @@ def get_prediction_history():
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() 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_conditions = []
@ -1737,8 +1767,9 @@ def get_prediction_history():
'id': record['id'], 'id': record['id'],
'prediction_uid': record['prediction_uid'], 'prediction_uid': record['prediction_uid'],
'product_id': product_id, 'product_id': product_id,
'product_name': product_name_map.get(product_id, record['product_name']), 'product_name': record['product_name'],
'model_type': record['model_type'], 'model_type': record['model_type'],
'model_version': record['model_version'] if 'model_version' in record.keys() else 'N/A',
'model_id': record['model_id'], 'model_id': record['model_id'],
'start_date': start_date_str, 'start_date': start_date_str,
'future_days': future_days, 'future_days': future_days,
@ -1766,7 +1797,7 @@ def get_prediction_history():
@app.route('/api/prediction/history/<prediction_id>', methods=['GET']) @app.route('/api/prediction/history/<prediction_id>', methods=['GET'])
def get_prediction_details(prediction_id): def get_prediction_details(prediction_id):
"""获取特定预测记录的详情 (v7 - 统一前端逻辑后的最终版)""" """获取特定预测记录的详情 (v8 - 鲁棒性增强)"""
try: try:
logger.info(f"正在获取预测记录详情ID: {prediction_id}") logger.info(f"正在获取预测记录详情ID: {prediction_id}")
@ -1781,75 +1812,73 @@ def get_prediction_details(prediction_id):
logger.warning(f"数据库中未找到预测记录: ID={prediction_id}") logger.warning(f"数据库中未找到预测记录: ID={prediction_id}")
return jsonify({"status": "error", "message": "预测记录不存在"}), 404 return jsonify({"status": "error", "message": "预测记录不存在"}), 404
file_path = record['file_path'] record_keys = record.keys()
if not file_path or not os.path.exists(file_path):
logger.error(f"预测结果文件不存在或路径为空: {file_path}")
return jsonify({"status": "error", "message": "预测结果文件不存在"}), 404
with open(file_path, 'r', encoding='utf-8') as f: # 安全地获取文件路径 (相对路径)
relative_file_path = record['result_file_path'] if 'result_file_path' in record_keys else None
# 构建文件的绝对路径以进行可靠的检查和读取
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
full_path = None
if relative_file_path:
full_path = os.path.join(project_root, relative_file_path)
if not full_path or not os.path.exists(full_path):
logger.error(f"预测结果文件不存在或路径无效。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'")
print(f"DEBUG: JSON文件路径检查失败。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'") # 添加调试打印
# 即使文件不存在,也尝试返回基本信息,避免前端崩溃
final_payload = {
'product_name': record['product_name'] if 'product_name' in record_keys else 'N/A',
'model_type': record['model_type'] if 'model_type' in record_keys else 'N/A',
'start_date': json.loads(record['prediction_params']).get('start_date', 'N/A') if 'prediction_params' in record_keys and record['prediction_params'] else 'N/A',
'created_at': record['created_at'] if 'created_at' in record_keys else 'N/A',
'history_data': [],
'prediction_data': [],
'analysis': {"description": "错误:找不到详细的预测数据文件。"},
}
return jsonify({"status": "success", "data": final_payload})
# 读取和解析JSON文件
print(f"DEBUG: 正在尝试读取JSON文件绝对路径: '{full_path}'") # 添加调试打印
with open(full_path, 'r', encoding='utf-8') as f:
saved_data = json.load(f) saved_data = json.load(f)
core_data = saved_data # 提取核心数据
if 'data' in saved_data and isinstance(saved_data.get('data'), dict): core_data = saved_data.get('data', saved_data)
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', []) history_data = core_data.get('history_data', [])
prediction_data = core_data.get('prediction_data', []) prediction_data = core_data.get('prediction_data', [])
cleaned_history = [] # 立即打印提取到的数据长度
for item in (history_data or []): print(f"DEBUG: Extracted data lengths - History: {len(history_data)}, Prediction: {len(prediction_data)}")
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 = { final_payload = {
'product_name': record['product_name'], 'product_name': record['product_name'] if 'product_name' in record_keys else 'N/A',
'model_type': record['model_type'], 'model_type': record['model_type'] if 'model_type' in record_keys else 'N/A',
'start_date': record['start_date'], 'start_date': json.loads(record['prediction_params']).get('start_date', 'N/A') if 'prediction_params' in record_keys and record['prediction_params'] else 'N/A',
'created_at': record['created_at'], 'created_at': record['created_at'] if 'created_at' in record_keys else 'N/A',
'history_data': cleaned_history, 'history_data': history_data,
'prediction_data': cleaned_prediction, 'prediction_data': prediction_data,
'analysis': core_data.get('analysis', {}), 'analysis': core_data.get('analysis', {}),
} }
# 3. 最终封装
response_data = { response_data = {
"status": "success", "status": "success",
"data": final_payload "data": final_payload
} }
logger.info(f"成功构建并返回历史预测详情 (v7): ID={prediction_id}, " # 在返回前,完整打印最终的响应数据
f"历史数据点: {len(final_payload['history_data'])}, " print(f"DEBUG: Final response payload being sent to frontend: {json.dumps(response_data, cls=CustomJSONEncoder, ensure_ascii=False)}")
f"预测数据点: {len(final_payload['prediction_data'])}") logger.info(f"成功构建并返回历史预测详情 (v8): ID={prediction_id}")
return jsonify(response_data) return jsonify(response_data)
except json.JSONDecodeError as e:
logger.error(f"预测结果文件JSON解析错误: {file_path}, 错误: {e}")
return jsonify({"status": "error", "message": f"预测结果文件格式错误: {str(e)}"}), 500
except Exception as e: except Exception as e:
logger.error(f"获取预测详情失败: {str(e)}") logger.error(f"获取预测详情失败: {str(e)}")
traceback.print_exc() traceback.print_exc()
return jsonify({"status": "error", "message": str(e)}), 500 return jsonify({"status": "error", "message": f"获取预测详情时发生内部错误: {str(e)}"}), 500
@app.route('/api/prediction/history/<prediction_id>', methods=['DELETE']) @app.route('/api/prediction/history/<prediction_id>', methods=['DELETE'])
def delete_prediction(prediction_id): def delete_prediction(prediction_id):

View File

@ -122,14 +122,15 @@ def save_prediction_to_db(prediction_data: dict):
try: try:
cursor.execute(''' cursor.execute('''
INSERT INTO prediction_history ( INSERT INTO prediction_history (
prediction_uid, model_id, model_type, product_name, prediction_uid, model_id, model_type, product_name, model_version,
prediction_scope, prediction_params, metrics, result_file_path prediction_scope, prediction_params, metrics, result_file_path
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
prediction_data.get('prediction_uid'), prediction_data.get('prediction_uid'),
prediction_data.get('model_id'), # This should be the model_uid from the 'models' table prediction_data.get('model_id'), # This should be the model_uid from the 'models' table
prediction_data.get('model_type'), prediction_data.get('model_type'),
prediction_data.get('product_name'), prediction_data.get('product_name'),
prediction_data.get('model_version'),
json.dumps(prediction_data.get('prediction_scope')), json.dumps(prediction_data.get('prediction_scope')),
json.dumps(prediction_data.get('prediction_params')), json.dumps(prediction_data.get('prediction_params')),
json.dumps(prediction_data.get('metrics')), json.dumps(prediction_data.get('metrics')),

View File

@ -53,3 +53,4 @@
| `metrics` | TEXT (JSON) | **已确认 (冗余).** 缓存的性能指标,用于列表展示和排序。 | | `metrics` | TEXT (JSON) | **已确认 (冗余).** 缓存的性能指标,用于列表展示和排序。 |
| `result_file_path` | TEXT | **[已采纳您的规范]** 指向预测结果JSON文件的**相对路径**。文件存储在 `saved_predictions/` 目录下,并根据模型名和时间戳命名,例如:`saved_predictions/cnn_bilstm_attention_global_sum_v6_pred_20250724111600.json`。 | | `result_file_path` | TEXT | **[已采纳您的规范]** 指向预测结果JSON文件的**相对路径**。文件存储在 `saved_predictions/` 目录下,并根据模型名和时间戳命名,例如:`saved_predictions/cnn_bilstm_attention_global_sum_v6_pred_20250724111600.json`。 |
| `created_at` | DATETIME | **已确认.** 记录的创建时间。 | | `created_at` | DATETIME | **已确认.** 记录的创建时间。 |
| `model_version` | TEXT |