解决历史预测界面数据读取出错,详情还是无法加载
This commit is contained in:
parent
adb5e0f2b4
commit
d5ec662070
@ -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>
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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"
|
||||||
|
@ -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('获取模型列表失败')
|
||||||
}
|
}
|
||||||
|
@ -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.
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')
|
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):
|
||||||
|
@ -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')),
|
||||||
|
@ -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 |
|
||||||
|
Loading…
x
Reference in New Issue
Block a user