完善历史预测查看详情,删除功能

This commit is contained in:
xz2000 2025-07-25 16:07:05 +08:00
parent 290e402181
commit 88f245b957
3 changed files with 217 additions and 157 deletions

View File

@ -14,7 +14,7 @@
<el-form :model="filters" inline class="filter-form">
<el-form-item label="产品">
<el-select v-model="filters.product_id" placeholder="筛选产品" filterable clearable @change="fetchHistory">
<el-option v-for="item in products" :key="item.product_id" :label="item.product_name" :value="item.product_id" />
<el-option v-for="item in productFilterOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="模型类型">
@ -82,18 +82,18 @@
</el-card>
<!-- 详情弹窗 -->
<el-dialog v-model="detailsVisible" title="预测详情" fullscreen destroy-on-close>
<el-dialog v-model="detailsVisible" title="预测详情" fullscreen destroy-on-close @close="destroyCharts">
<div v-if="currentPrediction" class="prediction-dialog-content">
<el-row :gutter="20">
<el-col :span="24">
<div class="prediction-summary">
<el-descriptions :column="4" border>
<el-descriptions-item label="产品名称">{{ currentPrediction.meta?.product_name || currentPrediction.product_name || getProductName(currentPrediction.meta?.product_id) || '未知产品' }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ currentPrediction.product_name || '未知产品' }}</el-descriptions-item>
<el-descriptions-item label="模型类型">
<el-tag :type="getModelTagType(currentPrediction.meta?.model_type || currentPrediction.model_type)">{{ currentPrediction.meta?.model_type || currentPrediction.model_type || '未知模型' }}</el-tag>
<el-tag :type="getModelTagType(currentPrediction.model_type)">{{ currentPrediction.model_type || '未知模型' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预测起始日">{{ currentPrediction.meta?.start_date || currentPrediction.start_date || (currentPrediction.data?.prediction_data?.[0]?.date) || 'N/A' }}</el-descriptions-item>
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.meta?.created_at || currentPrediction.created_at) }}</el-descriptions-item>
<el-descriptions-item label="预测起始日">{{ currentPrediction.start_date || 'N/A' }}</el-descriptions-item>
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.created_at) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-col>
@ -107,7 +107,7 @@
<div class="prediction-chart-container">
<h3>预测趋势图</h3>
<el-card shadow="hover" body-style="padding: 0;">
<div id="fullscreen-prediction-chart-history" style="width: 100%; height: 500px;"></div>
<canvas ref="predictionChartCanvas" style="width: 100%; height: 500px;"></canvas>
</el-card>
</div>
</el-col>
@ -123,29 +123,29 @@
<div class="stat-cards">
<el-card shadow="hover">
<template #header><div class="stat-header"><span>平均预测销量</span></div></template>
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateAverage(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateAverage(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
</el-card>
<el-card shadow="hover">
<template #header><div class="stat-header"><span>最高预测销量</span></div></template>
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMax(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMax(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
</el-card>
<el-card shadow="hover">
<template #header><div class="stat-header"><span>最低预测销量</span></div></template>
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMin(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMin(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
</el-card>
</div>
<el-table :data="currentPrediction?.data?.prediction_data || []" stripe height="330" border>
<el-table :data="currentPrediction?.prediction_data || []" stripe height="330" border>
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
<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.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>
<div v-if="$index > 0 && currentPrediction?.prediction_data">
<el-icon v-if="row.predicted_sales > currentPrediction.prediction_data[$index-1].predicted_sales" color="#67C23A"><ArrowUp /></el-icon>
<el-icon v-else-if="row.predicted_sales < currentPrediction.prediction_data[$index-1].predicted_sales" color="#F56C6C"><ArrowDown /></el-icon>
<el-icon v-else color="#909399"><Minus /></el-icon>
{{ Math.abs(row.predicted_sales - currentPrediction.data.prediction_data[$index-1].predicted_sales).toFixed(2) }}
{{ Math.abs(row.predicted_sales - currentPrediction.prediction_data[$index-1].predicted_sales).toFixed(2) }}
</div>
<span v-else>-</span>
</template>
@ -153,7 +153,7 @@
</el-table>
</el-tab-pane>
<el-tab-pane label="历史数据">
<el-table :data="currentPrediction?.data?.history_data || []" stripe height="400" border>
<el-table :data="currentPrediction?.history_data || []" stripe height="400" 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>
@ -161,7 +161,7 @@
</el-table>
</el-tab-pane>
<el-tab-pane label="全部数据">
<el-table :data="currentPrediction?.data?.data || []" stripe height="400" border>
<el-table :data="[...(currentPrediction?.history_data || []).map(d => ({...d, data_type: '历史销量'})), ...(currentPrediction?.prediction_data || []).map(d => ({...d, sales: d.predicted_sales, data_type: '预测销量'}))]" stripe height="400" 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>
@ -212,7 +212,7 @@
<el-col :span="24">
<el-card shadow="hover" style="margin-bottom: 20px;">
<template #header><div class="analysis-header"><span>日环比变化</span></div></template>
<div id="fullscreen-history-chart-history" style="width: 100%; height: 350px;"></div>
<canvas ref="analysisChartCanvas" style="width: 100%; height: 350px;"></canvas>
</el-card>
</el-col>
</el-row>
@ -254,15 +254,17 @@ import { computed, onUnmounted } from 'vue';
const loading = ref(false);
const history = ref([]);
const products = ref([]);
const productFilterOptions = ref([]);
const modelTypes = ref([]);
const detailsVisible = ref(false);
const currentPrediction = ref(null);
const rawResponseData = ref(null);
const showRawDataFlag = ref(false);
const predictionChartCanvas = ref(null);
const analysisChartCanvas = ref(null);
let predictionChart = null; // << 使chart
let historyChart = null;
let predictionChartInstance = null;
let analysisChartInstance = null;
const filters = reactive({
product_id: '',
@ -287,14 +289,14 @@ const fetchModelTypes = async () => {
}
};
const fetchProducts = async () => {
const fetchFilterOptions = async () => {
try {
const response = await axios.get('/api/products');
const response = await axios.get('/api/history/filter-options');
if (response.data.status === 'success') {
products.value = response.data.data;
productFilterOptions.value = response.data.data;
}
} catch (error) {
ElMessage.error('获取产品列表失败');
ElMessage.error('获取筛选选项列表失败');
console.error(error);
}
};
@ -311,7 +313,6 @@ const fetchHistory = async () => {
if (response.data.status === 'success') {
history.value = response.data.data;
pagination.total = response.data.total;
}
} catch (error) {
ElMessage.error('获取历史记录失败');
@ -333,8 +334,8 @@ const handleCurrentChange = (page) => {
// getProductName
const getProductName = (productId) => {
if (!productId) return '未知产品';
const product = products.value.find(p => p.product_id === productId);
return product ? product.product_name : '未知产品';
const option = productFilterOptions.value.find(p => p.value === productId);
return option ? option.label : '未知产品';
};
const viewDetails = async (id) => {
@ -345,11 +346,19 @@ const viewDetails = async (id) => {
if (responseData && responseData.status === 'success' && responseData.data) {
// responseData.data
//
// data
currentPrediction.value = responseData.data;
detailsVisible.value = true;
console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
// nextTickDOM
nextTick(() => {
renderPredictionChart(currentPrediction.value);
renderHistoryAnalysisChart(currentPrediction.value);
});
} else {
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
}
@ -903,155 +912,116 @@ const getFactorsArray = computed(() => {
return [];
});
watch(detailsVisible, (newVal) => {
if (newVal && currentPrediction.value) {
nextTick(() => {
renderChart();
//
// renderHistoryAnalysisChart();
});
}
});
const renderPredictionChart = (predictionData) => {
const canvasEl = predictionChartCanvas.value;
if (!canvasEl || !predictionData) return;
// << ProductPredictionView.vuerenderChart
const renderChart = () => {
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
if (predictionChart) {
predictionChart.destroy();
if (predictionChartInstance) {
predictionChartInstance.destroy();
}
const formatDate = (date) => new Date(date).toISOString().split('T')[0];
const historyData = (predictionData.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
const predData = (predictionData.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
const historyData = (currentPrediction.value.data.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
const predictionData = (currentPrediction.value.data.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
if (historyData.length === 0 && predictionData.length === 0) {
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);
if (historyData.length === 0 && predData.length === 0) {
// Handle no data case if needed
return;
}
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
const allLabels = [...new Set([...historyData.map(p => p.date), ...predData.map(p => p.date)])].sort();
const simplifiedLabels = allLabels.map(date => date.split('-').slice(1).join('/'));
const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
// 使 'predicted_sales'
const predictionMap = new Map(predictionData.map(p => [p.date, p.predicted_sales]));
const predictionMap = new Map(predData.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);
if (historyData.length > 0 && predictionData.length > 0) {
if (historyData.length > 0 && predData.length > 0) {
const lastHistoryDate = historyData[historyData.length - 1].date;
const lastHistoryValue = historyData[historyData.length - 1].sales;
if (!predictionMap.has(lastHistoryDate)) {
alignedPredictionSales[allLabels.indexOf(lastHistoryDate)] = lastHistoryValue;
const lastHistoryIndex = allLabels.indexOf(lastHistoryDate);
if (!predictionMap.has(lastHistoryDate) && lastHistoryIndex !== -1) {
alignedPredictionSales[lastHistoryIndex] = lastHistoryValue;
}
}
let subtitleText = '';
if (historyData.length > 0) {
subtitleText += `历史数据: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
}
if (predictionData.length > 0) {
if (historyData.length > 0) subtitleText += `历史: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
if (predData.length > 0) {
if (subtitleText) subtitleText += ' | ';
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
subtitleText += `预测: ${predData[0].date} ~ ${predData[predData.length - 1].date}`;
}
predictionChart = new Chart(chartCanvas, {
predictionChartInstance = new Chart(canvasEl, {
type: 'line',
data: {
labels: simplifiedLabels,
datasets: [
{
label: '历史销量',
data: alignedHistorySales,
borderColor: '#67C23A',
backgroundColor: 'rgba(103, 194, 58, 0.2)',
tension: 0.4,
fill: true,
spanGaps: false,
},
{
label: '预测销量',
data: alignedPredictionSales,
borderColor: '#409EFF',
backgroundColor: 'rgba(64, 158, 255, 0.2)',
tension: 0.4,
fill: true,
borderDash: [5, 5],
}
{ label: '历史销量', data: alignedHistorySales, borderColor: '#67C23A', backgroundColor: 'rgba(103, 194, 58, 0.2)', tension: 0.4, fill: true, spanGaps: false },
{ label: '预测销量', data: alignedPredictionSales, borderColor: '#409EFF', backgroundColor: 'rgba(64, 158, 255, 0.2)', tension: 0.4, fill: true, borderDash: [5, 5] }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`,
color: '#ffffff',
font: {
size: 20,
weight: 'bold',
}
},
subtitle: {
display: true,
text: subtitleText,
color: '#6c757d',
font: {
size: 14,
},
padding: {
bottom: 20
}
}
title: { display: true, text: `${predictionData.product_name} - 销量预测趋势图`, color: '#ffffff', font: { size: 20, weight: 'bold' } },
subtitle: { display: true, text: subtitleText, color: '#6c757d', font: { size: 14 }, padding: { bottom: 20 } }
},
scales: {
x: {
title: {
display: true,
text: '日期 (日)'
},
grid: {
display: false
}
},
y: {
title: {
display: true,
text: '销量'
},
grid: {
color: '#e9e9e9',
drawBorder: false,
},
beginAtZero: true
x: { title: { display: true, text: '日期' }, grid: { display: false } },
y: { title: { display: true, text: '销量' }, grid: { color: '#e9e9e9', drawBorder: false }, beginAtZero: true }
}
}
});
};
const renderHistoryAnalysisChart = (predictionData) => {
const canvasEl = analysisChartCanvas.value;
if (!canvasEl || !predictionData.analysis || !predictionData.analysis.history_chart_data) return;
if (analysisChartInstance) {
analysisChartInstance.destroy();
}
const chartData = predictionData.analysis.history_chart_data;
analysisChartInstance = new Chart(canvasEl, {
type: 'bar',
data: {
labels: chartData.dates.map(d => d.split('-').slice(1).join('/')),
datasets: [{
label: '日环比变化 (%)',
data: chartData.changes,
backgroundColor: chartData.changes.map(c => c >= 0 ? 'rgba(103, 194, 58, 0.6)' : 'rgba(245, 108, 108, 0.6)'),
borderColor: chartData.changes.map(c => c >= 0 ? '#67C23A' : '#F56C6C'),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { title: { display: true, text: '变化率 (%)' } } }
}
});
};
const exportHistoryData = () => {
if (!currentPrediction.value) return;
const data = currentPrediction.value.data.data;
const allData = [
...(currentPrediction.value.history_data || []).map(d => ({...d, data_type: '历史销量'})),
...(currentPrediction.value.prediction_data || []).map(d => ({...d, sales: d.predicted_sales, data_type: '预测销量'}))
];
let csvContent = "日期,销量,数据类型\n";
data.forEach(row => {
csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`;
allData.forEach(row => {
const salesValue = row.sales || row.predicted_sales || 0;
csvContent += `${new Date(row.date).toLocaleDateString()},${salesValue.toFixed(2)},${row.data_type}\n`;
});
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
const { product_name, model_type, start_date } = currentPrediction.value.meta;
const { product_name, model_type, start_date } = currentPrediction.value;
link.setAttribute('download', `历史预测_${product_name}_${model_type}_${start_date}.csv`);
document.body.appendChild(link);
link.click();
@ -1060,28 +1030,34 @@ const exportHistoryData = () => {
};
const resizeCharts = () => {
if (predictionChart) {
predictionChart.resize();
if (predictionChartInstance) {
predictionChartInstance.resize();
}
if (historyChart) {
historyChart.resize();
if (analysisChartInstance) {
analysisChartInstance.resize();
}
};
window.addEventListener('resize', resizeCharts);
const destroyCharts = () => {
if (predictionChartInstance) {
predictionChartInstance.destroy();
predictionChartInstance = null;
}
if (analysisChartInstance) {
analysisChartInstance.destroy();
analysisChartInstance = null;
}
};
onUnmounted(() => {
window.removeEventListener('resize', resizeCharts);
if (predictionChart) {
predictionChart.destroy();
}
if (historyChart) {
historyChart.destroy();
}
destroyCharts(); //
});
onMounted(() => {
fetchProducts();
fetchFilterOptions();
fetchModelTypes();
fetchHistory();
});

Binary file not shown.

View File

@ -1398,6 +1398,24 @@ def predict():
if prediction_result is None:
return jsonify({"status": "error", "message": "预测失败核心预测器返回None"}), 500
# 调试步骤:在分析函数调用后,打印其输入和输出
history_data_for_analysis = prediction_result.get('history_data', [])
prediction_data_for_analysis = prediction_result.get('prediction_data', [])
logger.info(f"DEBUG: 分析函数的输入参数检查 - "
f"history_data 类型: {type(history_data_for_analysis)}, 长度: {len(history_data_for_analysis)}. "
f"prediction_data 类型: {type(prediction_data_for_analysis)}, 长度: {len(prediction_data_for_analysis)}.")
logger.info(f"DEBUG: 核心预测器返回的 'analysis' 字段内容: {prediction_result.get('analysis')}")
# 根本修复:确保 'analysis' 字段始终存在且结构正确
if not prediction_result.get('analysis'):
logger.warning(f"模型 {model_uid} 的预测结果中缺少分析数据,将使用默认空对象。")
prediction_result['analysis'] = {
'description': '未能生成有效的趋势分析。',
'metrics': {},
'peaks': [],
'history_chart_data': {'dates': [], 'changes': []}
}
# 遵循用户规范,使用相对路径生成文件名
model_display_name = model_record.get('display_name', 'unknown_model')
safe_model_name = secure_filename(model_display_name).replace(' ', '_').replace('-_', '_')
@ -1733,12 +1751,25 @@ def get_prediction_history():
query_params = []
if product_id:
query_conditions.append("json_extract(prediction_scope, '$.product_id') = ?")
# v12 修复:支持混合类型的筛选
# 判断传入的ID类型产品ID(数字), 店铺ID(S开头), 或特殊名称(其他字符串)
if product_id.isdigit():
# 按产品ID筛选
query_conditions.append("prediction_scope LIKE ?")
query_params.append(f'%"product_id": "{product_id}"%')
elif product_id.startswith('S') and product_id[1:].isdigit():
# 按店铺ID筛选
query_conditions.append("prediction_scope LIKE ?")
query_params.append(f'%"store_id": "{product_id}"%')
else:
# 按特殊名称筛选 (如 全局预测)
query_conditions.append("product_name = ?")
query_params.append(product_id)
if model_type:
query_conditions.append("model_type = ?")
query_params.append(model_type)
# v10 修复: 使用LIKE以兼容(best)版本
query_conditions.append("model_type LIKE ?")
query_params.append(f"{model_type}%")
# 构建完整的查询语句
query = "SELECT * FROM prediction_history"
@ -1846,7 +1877,6 @@ def get_prediction_details(prediction_id):
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',
@ -1860,7 +1890,6 @@ def get_prediction_details(prediction_id):
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)
@ -1871,8 +1900,6 @@ def get_prediction_details(prediction_id):
history_data = core_data.get('history_data', [])
prediction_data = core_data.get('prediction_data', [])
# 立即打印提取到的数据长度
print(f"DEBUG: Extracted data lengths - History: {len(history_data)}, Prediction: {len(prediction_data)}")
# 构建最终的、完整的响应数据
final_payload = {
@ -1885,13 +1912,26 @@ def get_prediction_details(prediction_id):
'analysis': core_data.get('analysis', {}),
}
# 动态修复:如果从文件中加载的分析数据为空,则实时生成
if not final_payload.get('analysis'):
logger.info(f"记录 {prediction_id} 的分析数据为空,正在尝试动态生成...")
# 构建调用 analyze_prediction 所需的输入
dynamic_analysis_input = {
"history_data": final_payload.get('history_data', []),
"prediction_data": final_payload.get('prediction_data', [])
}
dynamic_analysis_result = analyze_prediction(dynamic_analysis_input)
if dynamic_analysis_result:
final_payload['analysis'] = dynamic_analysis_result
logger.info(f"成功为记录 {prediction_id} 动态生成分析数据。")
else:
logger.warning(f"为记录 {prediction_id} 动态生成分析数据失败。")
response_data = {
"status": "success",
"data": final_payload
}
# 在返回前,完整打印最终的响应数据
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)
@ -1909,14 +1949,15 @@ def delete_prediction(prediction_id):
cursor = conn.cursor()
# 查询预测记录
cursor.execute("SELECT file_path FROM prediction_history WHERE id = ?", (prediction_id,))
# v15 修复: 修正列名,并使用绝对路径删除文件
cursor.execute("SELECT result_file_path FROM prediction_history WHERE id = ?", (prediction_id,))
record = cursor.fetchone()
if not record:
conn.close()
return jsonify({"status": "error", "message": "预测记录不存在"}), 404
file_path = record[0]
relative_file_path = record[0]
# 删除数据库记录
cursor.execute("DELETE FROM prediction_history WHERE id = ?", (prediction_id,))
@ -1924,8 +1965,19 @@ def delete_prediction(prediction_id):
conn.close()
# 删除预测结果文件
if os.path.exists(file_path):
os.remove(file_path)
if relative_file_path:
# 构建绝对路径以确保文件能被正确找到和删除
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
absolute_file_path = os.path.join(project_root, relative_file_path)
if os.path.exists(absolute_file_path):
try:
os.remove(absolute_file_path)
logger.info(f"成功删除文件: {absolute_file_path}")
except OSError as e:
logger.error(f"删除文件失败: {absolute_file_path}, 错误: {e}")
else:
logger.warning(f"尝试删除但文件未找到: {absolute_file_path}")
return jsonify({
"status": "success",
@ -1937,6 +1989,38 @@ def delete_prediction(prediction_id):
traceback.print_exc()
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/api/history/filter-options', methods=['GET'])
def get_history_filter_options():
"""获取历史记录页面用于筛选的选项列表"""
try:
# 1. 获取所有标准产品
products = get_available_products()
# 2. 获取所有店铺
stores = get_available_stores()
# 3. 从历史记录中获取特殊的预测名称(如全局预测)
options_map = {}
# 添加产品
for p in products:
options_map[p['product_id']] = {'value': p['product_id'], 'label': p['product_name'], 'type': 'product'}
# 添加店铺
for s in stores:
options_map[s['store_id']] = {'value': s['store_id'], 'label': s['store_name'], 'type': 'store'}
# v14 修复: 硬编码添加“全局预测”选项
global_prediction_key = "全局预测"
if global_prediction_key not in options_map:
options_map[global_prediction_key] = {'value': global_prediction_key, 'label': global_prediction_key, 'type': 'special'}
return jsonify({"status": "success", "data": list(options_map.values())})
except Exception as e:
logger.error(f"获取历史筛选选项失败: {e}\n{traceback.format_exc()}")
return jsonify({"status": "error", "message": str(e)}), 500
# 4. 模型管理API
@app.route('/api/models', methods=['GET'])
@swag_from({