解决历史预测界面数据读取出错,详情还是无法加载
This commit is contained in:
parent
adb5e0f2b4
commit
d5ec662070
@ -225,10 +225,11 @@ onMounted(() => {
|
||||
|
||||
// 监听店铺变化,重新获取产品列表
|
||||
watch(() => props.storeId, () => {
|
||||
if (props.storeId !== null) {
|
||||
if (props.storeId !== null && !props.dataSource) {
|
||||
fetchProducts()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -38,20 +38,21 @@
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="history.length === 0 && !loading" description="暂无历史预测记录"></el-empty>
|
||||
<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="model_type" label="模型类型" min-width="120">
|
||||
<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" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getModelTagType(row.model_type)" size="small">{{ row.model_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_date" label="预测起始日" min-width="120" />
|
||||
<el-table-column prop="future_days" label="预测天数" min-width="100" />
|
||||
<el-table-column prop="created_at" label="预测时间" min-width="180">
|
||||
<el-table-column prop="model_version" label="模型版本" min-width="180" show-overflow-tooltip align="center" />
|
||||
<el-table-column prop="start_date" label="预测起始日" min-width="120" show-overflow-tooltip align="center" />
|
||||
<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 }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<el-button size="small" type="primary" @click="viewDetails(row.id)">
|
||||
@ -135,16 +136,16 @@
|
||||
</div>
|
||||
<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="sales" label="预测销量" sortable>
|
||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||
<el-table-column prop="predicted_sales" label="预测销量" sortable>
|
||||
<template #default="{ row }">{{ row.predicted_sales ? row.predicted_sales.toFixed(2) : '0.00' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="趋势">
|
||||
<template #default="{ row, $index }">
|
||||
<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-else-if="row.sales < currentPrediction.data.prediction_data[$index-1].sales" color="#F56C6C"><ArrowDown /></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.predicted_sales < currentPrediction.data.prediction_data[$index-1].predicted_sales" color="#F56C6C"><ArrowDown /></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>
|
||||
<span v-else>-</span>
|
||||
</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 () => {
|
||||
loading.value = true;
|
||||
@ -301,16 +312,6 @@ const fetchHistory = async () => {
|
||||
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('获取历史记录失败');
|
||||
@ -342,65 +343,12 @@ const viewDetails = async (id) => {
|
||||
const responseData = response.data;
|
||||
rawResponseData.value = JSON.stringify(responseData, null, 2);
|
||||
|
||||
if (responseData && responseData.status === 'success') {
|
||||
// 后端已经返回标准格式的数据,直接使用
|
||||
// 但仍需确保数据结构的完整性
|
||||
|
||||
// 确保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;
|
||||
if (responseData && responseData.status === 'success' && responseData.data) {
|
||||
// 简化逻辑:直接信任后端返回的、结构正确的 responseData.data
|
||||
currentPrediction.value = responseData.data;
|
||||
detailsVisible.value = true;
|
||||
|
||||
console.log('预测详情数据:', responseData);
|
||||
console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
|
||||
|
||||
} else {
|
||||
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
||||
@ -860,17 +808,23 @@ const parseJSONWithNaN = (jsonString) => {
|
||||
|
||||
const calculateAverage = (data) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
@ -974,7 +928,12 @@ const renderChart = () => {
|
||||
const predictionData = (currentPrediction.value.data.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -982,8 +941,8 @@ const renderChart = () => {
|
||||
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]));
|
||||
// 修正:预测数据使用 'predicted_sales' 字段
|
||||
const predictionMap = new Map(predictionData.map(p => [p.date, p.predicted_sales]));
|
||||
|
||||
const alignedHistorySales = allLabels.map(label => historyMap.get(label) ?? null);
|
||||
const alignedPredictionSales = allLabels.map(label => predictionMap.get(label) ?? null);
|
||||
@ -1122,7 +1081,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// fetchProducts(); // 不再需要独立获取产品列表
|
||||
fetchProducts();
|
||||
fetchModelTypes();
|
||||
fetchHistory();
|
||||
});
|
||||
|
@ -16,10 +16,10 @@
|
||||
</template>
|
||||
|
||||
<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-form-item>
|
||||
<el-form-item label="模型类型">
|
||||
<el-form-item label="模型类型" style="width:300px">
|
||||
<el-select v-model="filters.model_type" placeholder="按模型类型筛选" clearable>
|
||||
<el-option
|
||||
v-for="item in modelTypes"
|
||||
|
@ -153,6 +153,7 @@ const fetchModels = async () => {
|
||||
const response = await axios.get('/api/models', { params: { training_mode: 'global' } })
|
||||
if (response.data.status === 'success') {
|
||||
modelList.value = response.data.data
|
||||
console.log('全局模型列表:', response.data.data)
|
||||
} else {
|
||||
ElMessage.error('获取模型列表失败')
|
||||
}
|
||||
|
@ -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 { QuestionFilled, TrendCharts } from '@element-plus/icons-vue'
|
||||
import Chart from 'chart.js/auto'
|
||||
import ProductSelector from '../../components/ProductSelector.vue'
|
||||
|
||||
const modelList = ref([])
|
||||
const modelTypes = ref([])
|
||||
|
Binary file not shown.
151
server/api.py
151
server/api.py
@ -171,6 +171,8 @@ def init_db():
|
||||
cursor.execute('ALTER TABLE prediction_history ADD COLUMN prediction_scope TEXT')
|
||||
if 'result_file_path' not in columns:
|
||||
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 字段存在且类型正确
|
||||
# 在SQLite中修改列类型比较复杂,通常建议重建。此处简化处理。
|
||||
|
||||
@ -1340,8 +1342,16 @@ def predict():
|
||||
# 解析 artifacts 找到模型文件路径
|
||||
artifacts = json.loads(model_record.get('artifacts', '{}'))
|
||||
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 = json.loads(model_record.get('training_scope', '{}'))
|
||||
@ -1356,7 +1366,7 @@ def predict():
|
||||
|
||||
# 调用核心预测函数
|
||||
prediction_result = load_model_and_predict(
|
||||
model_path=model_file_path,
|
||||
model_path=absolute_model_path,
|
||||
product_id=product_id,
|
||||
model_type=model_type,
|
||||
store_id=store_id,
|
||||
@ -1371,22 +1381,46 @@ def predict():
|
||||
if prediction_result is None:
|
||||
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())
|
||||
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:
|
||||
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 = {
|
||||
"prediction_uid": prediction_uid,
|
||||
"model_id": model_uid,
|
||||
"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_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
|
||||
}
|
||||
save_prediction_to_db(db_payload)
|
||||
@ -1673,10 +1707,6 @@ 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 = []
|
||||
@ -1737,8 +1767,9 @@ def get_prediction_history():
|
||||
'id': record['id'],
|
||||
'prediction_uid': record['prediction_uid'],
|
||||
'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_version': record['model_version'] if 'model_version' in record.keys() else 'N/A',
|
||||
'model_id': record['model_id'],
|
||||
'start_date': start_date_str,
|
||||
'future_days': future_days,
|
||||
@ -1766,7 +1797,7 @@ def get_prediction_history():
|
||||
|
||||
@app.route('/api/prediction/history/<prediction_id>', methods=['GET'])
|
||||
def get_prediction_details(prediction_id):
|
||||
"""获取特定预测记录的详情 (v7 - 统一前端逻辑后的最终版)"""
|
||||
"""获取特定预测记录的详情 (v8 - 鲁棒性增强)"""
|
||||
try:
|
||||
logger.info(f"正在获取预测记录详情,ID: {prediction_id}")
|
||||
|
||||
@ -1781,75 +1812,73 @@ def get_prediction_details(prediction_id):
|
||||
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
|
||||
record_keys = record.keys()
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
# 提取核心数据
|
||||
core_data = saved_data.get('data', saved_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
|
||||
})
|
||||
# 立即打印提取到的数据长度
|
||||
print(f"DEBUG: Extracted data lengths - History: {len(history_data)}, Prediction: {len(prediction_data)}")
|
||||
|
||||
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,
|
||||
'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': history_data,
|
||||
'prediction_data': prediction_data,
|
||||
'analysis': core_data.get('analysis', {}),
|
||||
}
|
||||
|
||||
# 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'])}")
|
||||
|
||||
# 在返回前,完整打印最终的响应数据
|
||||
print(f"DEBUG: Final response payload being sent to frontend: {json.dumps(response_data, cls=CustomJSONEncoder, ensure_ascii=False)}")
|
||||
logger.info(f"成功构建并返回历史预测详情 (v8): ID={prediction_id}")
|
||||
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:
|
||||
logger.error(f"获取预测详情失败: {str(e)}")
|
||||
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'])
|
||||
def delete_prediction(prediction_id):
|
||||
|
@ -122,14 +122,15 @@ def save_prediction_to_db(prediction_data: dict):
|
||||
try:
|
||||
cursor.execute('''
|
||||
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
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
prediction_data.get('prediction_uid'),
|
||||
prediction_data.get('model_id'), # This should be the model_uid from the 'models' table
|
||||
prediction_data.get('model_type'),
|
||||
prediction_data.get('product_name'),
|
||||
prediction_data.get('model_version'),
|
||||
json.dumps(prediction_data.get('prediction_scope')),
|
||||
json.dumps(prediction_data.get('prediction_params')),
|
||||
json.dumps(prediction_data.get('metrics')),
|
||||
|
@ -53,3 +53,4 @@
|
||||
| `metrics` | TEXT (JSON) | **已确认 (冗余).** 缓存的性能指标,用于列表展示和排序。 |
|
||||
| `result_file_path` | TEXT | **[已采纳您的规范]** 指向预测结果JSON文件的**相对路径**。文件存储在 `saved_predictions/` 目录下,并根据模型名和时间戳命名,例如:`saved_predictions/cnn_bilstm_attention_global_sum_v6_pred_20250724111600.json`。 |
|
||||
| `created_at` | DATETIME | **已确认.** 记录的创建时间。 |
|
||||
| `model_version` | TEXT |
|
||||
|
Loading…
x
Reference in New Issue
Block a user