Compare commits
2 Commits
290e402181
...
af7638aeca
Author | SHA1 | Date | |
---|---|---|---|
af7638aeca | |||
88f245b957 |
@ -14,7 +14,7 @@
|
|||||||
<el-form :model="filters" inline class="filter-form">
|
<el-form :model="filters" inline class="filter-form">
|
||||||
<el-form-item label="产品">
|
<el-form-item label="产品">
|
||||||
<el-select v-model="filters.product_id" placeholder="筛选产品" filterable clearable @change="fetchHistory">
|
<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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型类型">
|
<el-form-item label="模型类型">
|
||||||
@ -82,18 +82,18 @@
|
|||||||
</el-card>
|
</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">
|
<div v-if="currentPrediction" class="prediction-dialog-content">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div class="prediction-summary">
|
<div class="prediction-summary">
|
||||||
<el-descriptions :column="4" border>
|
<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-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>
|
||||||
<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="预测起始日">{{ currentPrediction.start_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="预测时间">{{ formatDateTime(currentPrediction.created_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -107,7 +107,7 @@
|
|||||||
<div class="prediction-chart-container">
|
<div class="prediction-chart-container">
|
||||||
<h3>预测趋势图</h3>
|
<h3>预测趋势图</h3>
|
||||||
<el-card shadow="hover" body-style="padding: 0;">
|
<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>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -123,29 +123,29 @@
|
|||||||
<div class="stat-cards">
|
<div class="stat-cards">
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>平均预测销量</span></div></template>
|
<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>
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>最高预测销量</span></div></template>
|
<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>
|
||||||
<el-card shadow="hover">
|
<el-card shadow="hover">
|
||||||
<template #header><div class="stat-header"><span>最低预测销量</span></div></template>
|
<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>
|
</el-card>
|
||||||
</div>
|
</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="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="predicted_sales" label="预测销量" sortable>
|
<el-table-column prop="predicted_sales" label="预测销量" sortable>
|
||||||
<template #default="{ row }">{{ row.predicted_sales ? row.predicted_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?.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-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.data.prediction_data[$index-1].predicted_sales" color="#F56C6C"><ArrowDown /></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>
|
<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>
|
</div>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
@ -153,7 +153,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="历史数据">
|
<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="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="sales" label="历史销量" sortable>
|
<el-table-column prop="sales" label="历史销量" sortable>
|
||||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||||
@ -161,7 +161,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="全部数据">
|
<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="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||||
<el-table-column prop="sales" label="销量" sortable>
|
<el-table-column prop="sales" label="销量" sortable>
|
||||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||||
<template #header><div class="analysis-header"><span>日环比变化</span></div></template>
|
<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-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -254,15 +254,17 @@ import { computed, onUnmounted } from 'vue';
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const history = ref([]);
|
const history = ref([]);
|
||||||
const products = ref([]);
|
const productFilterOptions = ref([]);
|
||||||
const modelTypes = ref([]);
|
const modelTypes = ref([]);
|
||||||
const detailsVisible = ref(false);
|
const detailsVisible = ref(false);
|
||||||
const currentPrediction = ref(null);
|
const currentPrediction = ref(null);
|
||||||
const rawResponseData = ref(null);
|
const rawResponseData = ref(null);
|
||||||
const showRawDataFlag = ref(false);
|
const showRawDataFlag = ref(false);
|
||||||
|
const predictionChartCanvas = ref(null);
|
||||||
|
const analysisChartCanvas = ref(null);
|
||||||
|
|
||||||
let predictionChart = null; // << 关键改动:使用单个chart实例
|
let predictionChartInstance = null;
|
||||||
let historyChart = null;
|
let analysisChartInstance = null;
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
product_id: '',
|
product_id: '',
|
||||||
@ -287,14 +289,14 @@ const fetchModelTypes = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchFilterOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/products');
|
const response = await axios.get('/api/history/filter-options');
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
products.value = response.data.data;
|
productFilterOptions.value = response.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取产品列表失败');
|
ElMessage.error('获取筛选选项列表失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -311,7 +313,6 @@ const fetchHistory = async () => {
|
|||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
history.value = response.data.data;
|
history.value = response.data.data;
|
||||||
pagination.total = response.data.total;
|
pagination.total = response.data.total;
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取历史记录失败');
|
ElMessage.error('获取历史记录失败');
|
||||||
@ -333,8 +334,8 @@ const handleCurrentChange = (page) => {
|
|||||||
// 添加getProductName函数
|
// 添加getProductName函数
|
||||||
const getProductName = (productId) => {
|
const getProductName = (productId) => {
|
||||||
if (!productId) return '未知产品';
|
if (!productId) return '未知产品';
|
||||||
const product = products.value.find(p => p.product_id === productId);
|
const option = productFilterOptions.value.find(p => p.value === productId);
|
||||||
return product ? product.product_name : '未知产品';
|
return option ? option.label : '未知产品';
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewDetails = async (id) => {
|
const viewDetails = async (id) => {
|
||||||
@ -345,11 +346,19 @@ const viewDetails = async (id) => {
|
|||||||
|
|
||||||
if (responseData && responseData.status === 'success' && responseData.data) {
|
if (responseData && responseData.status === 'success' && responseData.data) {
|
||||||
// 简化逻辑:直接信任后端返回的、结构正确的 responseData.data
|
// 简化逻辑:直接信任后端返回的、结构正确的 responseData.data
|
||||||
|
// 关键修复:将后端返回的扁平化数据结构正确赋值
|
||||||
|
// 后端返回的 data 字段直接包含了所有需要的信息
|
||||||
currentPrediction.value = responseData.data;
|
currentPrediction.value = responseData.data;
|
||||||
detailsVisible.value = true;
|
detailsVisible.value = true;
|
||||||
|
|
||||||
console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
|
console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
|
||||||
|
|
||||||
|
// 最终修复:在数据加载和弹窗可见后,通过nextTick确保DOM渲染完毕再初始化图表
|
||||||
|
nextTick(() => {
|
||||||
|
renderPredictionChart(currentPrediction.value);
|
||||||
|
renderHistoryAnalysisChart(currentPrediction.value);
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
||||||
}
|
}
|
||||||
@ -903,155 +912,116 @@ const getFactorsArray = computed(() => {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(detailsVisible, (newVal) => {
|
const renderPredictionChart = (predictionData) => {
|
||||||
if (newVal && currentPrediction.value) {
|
const canvasEl = predictionChartCanvas.value;
|
||||||
nextTick(() => {
|
if (!canvasEl || !predictionData) return;
|
||||||
renderChart();
|
|
||||||
// 可以在这里添加渲染第二个图表的逻辑
|
|
||||||
// renderHistoryAnalysisChart();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// << 关键改动:从ProductPredictionView.vue复制并适应的renderChart函数
|
if (predictionChartInstance) {
|
||||||
const renderChart = () => {
|
predictionChartInstance.destroy();
|
||||||
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
|
|
||||||
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
|
|
||||||
|
|
||||||
if (predictionChart) {
|
|
||||||
predictionChart.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date) => new Date(date).toISOString().split('T')[0];
|
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) }));
|
if (historyData.length === 0 && predData.length === 0) {
|
||||||
const predictionData = (currentPrediction.value.data.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
// Handle no data case if needed
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
|
const allLabels = [...new Set([...historyData.map(p => p.date), ...predData.map(p => p.date)])].sort();
|
||||||
const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
|
const simplifiedLabels = allLabels.map(date => date.split('-').slice(1).join('/'));
|
||||||
|
|
||||||
const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
|
const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
|
||||||
// 修正:预测数据使用 'predicted_sales' 字段
|
const predictionMap = new Map(predData.map(p => [p.date, p.predicted_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);
|
||||||
|
|
||||||
if (historyData.length > 0 && predictionData.length > 0) {
|
if (historyData.length > 0 && predData.length > 0) {
|
||||||
const lastHistoryDate = historyData[historyData.length - 1].date;
|
const lastHistoryDate = historyData[historyData.length - 1].date;
|
||||||
const lastHistoryValue = historyData[historyData.length - 1].sales;
|
const lastHistoryValue = historyData[historyData.length - 1].sales;
|
||||||
if (!predictionMap.has(lastHistoryDate)) {
|
const lastHistoryIndex = allLabels.indexOf(lastHistoryDate);
|
||||||
alignedPredictionSales[allLabels.indexOf(lastHistoryDate)] = lastHistoryValue;
|
if (!predictionMap.has(lastHistoryDate) && lastHistoryIndex !== -1) {
|
||||||
|
alignedPredictionSales[lastHistoryIndex] = lastHistoryValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitleText = '';
|
let subtitleText = '';
|
||||||
if (historyData.length > 0) {
|
if (historyData.length > 0) subtitleText += `历史: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
|
||||||
subtitleText += `历史数据: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
|
if (predData.length > 0) {
|
||||||
}
|
if (subtitleText) subtitleText += ' | ';
|
||||||
if (predictionData.length > 0) {
|
subtitleText += `预测: ${predData[0].date} ~ ${predData[predData.length - 1].date}`;
|
||||||
if (subtitleText) subtitleText += ' | ';
|
|
||||||
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
predictionChart = new Chart(chartCanvas, {
|
predictionChartInstance = new Chart(canvasEl, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: simplifiedLabels,
|
labels: simplifiedLabels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{ label: '历史销量', data: alignedHistorySales, borderColor: '#67C23A', backgroundColor: 'rgba(103, 194, 58, 0.2)', tension: 0.4, fill: true, spanGaps: false },
|
||||||
label: '历史销量',
|
{ label: '预测销量', data: alignedPredictionSales, borderColor: '#409EFF', backgroundColor: 'rgba(64, 158, 255, 0.2)', tension: 0.4, fill: true, borderDash: [5, 5] }
|
||||||
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: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
title: {
|
title: { display: true, text: `${predictionData.product_name} - 销量预测趋势图`, color: '#ffffff', font: { size: 20, weight: 'bold' } },
|
||||||
display: true,
|
subtitle: { display: true, text: subtitleText, color: '#6c757d', font: { size: 14 }, padding: { bottom: 20 } }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: { title: { display: true, text: '日期' }, grid: { display: false } },
|
||||||
title: {
|
y: { title: { display: true, text: '销量' }, grid: { color: '#e9e9e9', drawBorder: false }, beginAtZero: true }
|
||||||
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 = () => {
|
const exportHistoryData = () => {
|
||||||
if (!currentPrediction.value) return;
|
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";
|
let csvContent = "日期,销量,数据类型\n";
|
||||||
data.forEach(row => {
|
allData.forEach(row => {
|
||||||
csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`;
|
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 blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
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`);
|
link.setAttribute('download', `历史预测_${product_name}_${model_type}_${start_date}.csv`);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
@ -1060,28 +1030,34 @@ const exportHistoryData = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resizeCharts = () => {
|
const resizeCharts = () => {
|
||||||
if (predictionChart) {
|
if (predictionChartInstance) {
|
||||||
predictionChart.resize();
|
predictionChartInstance.resize();
|
||||||
}
|
}
|
||||||
if (historyChart) {
|
if (analysisChartInstance) {
|
||||||
historyChart.resize();
|
analysisChartInstance.resize();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', resizeCharts);
|
window.addEventListener('resize', resizeCharts);
|
||||||
|
|
||||||
|
const destroyCharts = () => {
|
||||||
|
if (predictionChartInstance) {
|
||||||
|
predictionChartInstance.destroy();
|
||||||
|
predictionChartInstance = null;
|
||||||
|
}
|
||||||
|
if (analysisChartInstance) {
|
||||||
|
analysisChartInstance.destroy();
|
||||||
|
analysisChartInstance = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', resizeCharts);
|
window.removeEventListener('resize', resizeCharts);
|
||||||
if (predictionChart) {
|
destroyCharts(); // 在组件卸载时也调用
|
||||||
predictionChart.destroy();
|
|
||||||
}
|
|
||||||
if (historyChart) {
|
|
||||||
historyChart.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchProducts();
|
fetchFilterOptions();
|
||||||
fetchModelTypes();
|
fetchModelTypes();
|
||||||
fetchHistory();
|
fetchHistory();
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,22 @@
|
|||||||
|
|
||||||
<el-form :inline="true" @submit.prevent="fetchModels">
|
<el-form :inline="true" @submit.prevent="fetchModels">
|
||||||
<el-form-item label="产品ID" style="width:300px">
|
<el-form-item label="产品ID" style="width:300px">
|
||||||
<el-input v-model="filters.product_id" placeholder="按产品ID筛选" clearable></el-input>
|
<el-select
|
||||||
|
v-model="filters.product_id"
|
||||||
|
placeholder="按产品/店铺/全局筛选"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="searchOptions"
|
||||||
|
:loading="searchLoading"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in searchResults"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型类型" style="width:300px">
|
<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>
|
||||||
@ -127,7 +142,7 @@
|
|||||||
{{ selectedModelDetails.model_info.model_type }}
|
{{ selectedModelDetails.model_info.model_type }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="产品">{{ selectedModelDetails.model_info.product_name }}</el-descriptions-item>
|
<el-descriptions-item :label="selectedModelDetails.model_info.scopeLabel">{{ selectedModelDetails.model_info.scopeName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatDateTime(selectedModelDetails.model_info.created_at) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDateTime(selectedModelDetails.model_info.created_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
@ -207,6 +222,27 @@ const models = ref([])
|
|||||||
const modelTypes = ref([])
|
const modelTypes = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const filters = reactive({ product_id: '', model_type: '' })
|
const filters = reactive({ product_id: '', model_type: '' })
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const searchResults = ref([])
|
||||||
|
|
||||||
|
const searchOptions = async (query) => {
|
||||||
|
if (query) {
|
||||||
|
searchLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/management/filter-options', { params: { query } })
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
searchResults.value = response.data.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取筛选选项失败:', error)
|
||||||
|
searchResults.value = []
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchResults.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 分页相关
|
// 分页相关
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@ -290,29 +326,43 @@ const handlePageSizeChange = (pageSize) => {
|
|||||||
|
|
||||||
const viewDetails = async (model) => {
|
const viewDetails = async (model) => {
|
||||||
detailsDialogVisible.value = true;
|
detailsDialogVisible.value = true;
|
||||||
selectedModelDetails.value = null; // 重置
|
selectedModelDetails.value = null;
|
||||||
try {
|
try {
|
||||||
// 新逻辑: 直接使用行数据,因为列表API已返回足够信息
|
const response = await axios.get(`/api/models/${model.model_uid}`);
|
||||||
const details = {
|
|
||||||
model_info: {
|
|
||||||
model_id: model.model_uid,
|
|
||||||
model_type: model.model_type,
|
|
||||||
product_name: model.display_name,
|
|
||||||
created_at: model.created_at,
|
|
||||||
},
|
|
||||||
training_metrics: model.performance_metrics,
|
|
||||||
chart_data: {
|
|
||||||
loss_chart: model.artifacts?.loss_curve_data || { epochs: [], train_loss: [], test_loss: [] }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
selectedModelDetails.value = details;
|
|
||||||
nextTick(() => {
|
|
||||||
initLossChart();
|
|
||||||
});
|
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
const details = response.data.data;
|
const details = response.data.data;
|
||||||
details.training_metrics = normalizeMetricsKeys(details.training_metrics);
|
|
||||||
selectedModelDetails.value = details;
|
// 准备用于显示的数据结构
|
||||||
|
// 准备用于显示的数据结构
|
||||||
|
const model_info = {
|
||||||
|
model_id: details.model_uid,
|
||||||
|
model_type: details.model_type,
|
||||||
|
created_at: details.created_at,
|
||||||
|
scopeLabel: '范围', // 默认标签
|
||||||
|
scopeName: details.display_name // 默认名称
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.training_mode === 'product') {
|
||||||
|
model_info.scopeLabel = '产品';
|
||||||
|
model_info.scopeName = details.product_name || details.display_name;
|
||||||
|
} else if (details.training_mode === 'store') {
|
||||||
|
model_info.scopeLabel = '店铺';
|
||||||
|
model_info.scopeName = details.store_name || details.display_name;
|
||||||
|
} else if (details.training_mode === 'global') {
|
||||||
|
model_info.scopeLabel = '全局模型';
|
||||||
|
model_info.scopeName = details.display_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDetails = {
|
||||||
|
model_info,
|
||||||
|
training_metrics: normalizeMetricsKeys(details.performance_metrics),
|
||||||
|
chart_data: {
|
||||||
|
loss_chart: details.artifacts?.loss_curve_data || { epochs: [], train_loss: [], test_loss: [] }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedModelDetails.value = formattedDetails;
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initLossChart();
|
initLossChart();
|
||||||
});
|
});
|
||||||
|
82
data/old_5shops_50skus-据结构字典.md
Normal file
82
data/old_5shops_50skus-据结构字典.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
|
||||||
|
| 分类 | 字段名 | 数据类型 | 描述 | 来源 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **标识符** | `subbh` | String | 店铺唯一标识 | 骨架 |
|
||||||
|
| | `hh` | String | 商品唯一标识 | 骨架 |
|
||||||
|
| | `kdrq` | Date | 开单日期 (主键之一) | 骨架 |
|
||||||
|
| **核心指标** | `sales_quantity` | Float | 当日销售量 (无销售则为0) | 当日销售 |
|
||||||
|
| | `return_quantity` | Float | 当日退货量 (无销售则为0) | 当日销售 |
|
||||||
|
| | `net_sales_quantity` | Float | **当日净销售量 (目标变量)** | 当日销售 |
|
||||||
|
| | `gross_profit_total` | Float | 当日毛利 (无销售则为0) | 当日销售 |
|
||||||
|
| | `transaction_count` | Integer | 当日交易次数 (无销售则为0) | 当日销售 |
|
||||||
|
| **日期特征** | `date` | Date | 日期 (冗余字段) | 时序计算 |
|
||||||
|
| | `is_weekend` | Boolean | 是否为周末 (True/False) | 时序计算 |
|
||||||
|
| | `day_of_week` | Integer | 一周中的第几天 (0=周一, 6=周日) | 时序计算 |
|
||||||
|
| | `day_of_month` | Integer | 一月中的第几天 (1-31) | 时序计算 |
|
||||||
|
| | `day_of_year` | Integer | 一年中的第几天 (1-366) | 时序计算 |
|
||||||
|
| | `week_of_month` | Integer | 当月第几周 (1-5) | 时序计算 |
|
||||||
|
| | `month` | Integer | 月份 (1-12) | 时序计算 |
|
||||||
|
| | `quarter` | Integer | 季度 (1-4) | 时序计算 |
|
||||||
|
| | `is_holiday` | Boolean | 是否为节假日 (True/False) | 时序计算 |
|
||||||
|
| **生命周期特征** | `first_sale_date` | Date | SKU在店首次销售日期 | 生命周期 |
|
||||||
|
| | `last_sale_date` | Date | SKU在店末次销售日期 | 生命周期 |
|
||||||
|
| | `lifecycle_days` | Integer | SKU在店生命周期总天数 | 生命周期 |
|
||||||
|
| | `sample_category` | String | 生命周期分类 (new/medium/old) | 生命周期 |
|
||||||
|
| | `rolling_7d_valid` | Boolean | 7日滚动窗口是否有效 (距离首次销售>=7天) | 生命周期 |
|
||||||
|
| | `rolling_15d_valid` | Boolean | 15日滚动窗口是否有效 | 生命周期 |
|
||||||
|
| | `rolling_30d_valid` | Boolean | 30日滚动窗口是否有效 | 生命周期 |
|
||||||
|
| | `rolling_90d_valid` | Boolean | 90日滚动窗口是否有效 | 生命周期 |
|
||||||
|
| **滚动特征 (7天)** | `sales_quantity_rolling_mean_7d` | Float | 过去7日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_7d` | Float | 过去7日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_7d`| Float | 过去7日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_7d` | Float | 过去7日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_7d` | Float | 过去7日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_7d` | Float | 过去7日总净销量 | 历史滚动 |
|
||||||
|
| **滚动特征 (15天)** | `sales_quantity_rolling_mean_15d` | Float | 过去15日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_15d` | Float | 过去15日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_15d`| Float | 过去15日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_15d` | Float | 过去15日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_15d` | Float | 过去15日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_15d` | Float | 过去15日总净销量 | 历史滚动 |
|
||||||
|
| **滚动特征 (30天)** | `sales_quantity_rolling_mean_30d` | Float | 过去30日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_30d` | Float | 过去30日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_30d`| Float | 过去30日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_30d` | Float | 过去30日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_30d` | Float | 过去30日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_30d` | Float | 过去30日总净销量 | 历史滚动 |
|
||||||
|
| **滚动特征 (90天)** | `sales_quantity_rolling_mean_90d` | Float | 过去90日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_90d` | Float | 过去90日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_90d`| Float | 过去90日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_90d` | Float | 过去90日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_90d` | Float | 过去90日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_90d` | Float | 过去90日总净销量 | 历史滚动 |
|
||||||
|
| **滚动特征 (180天)** | `sales_quantity_rolling_mean_180d` | Float | 过去180日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_180d` | Float | 过去180日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_180d`| Float | 过去180日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_180d` | Float | 过去180日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_180d` | Float | 过去180日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_180d` | Float | 过去180日总净销量 | 历史滚动 |
|
||||||
|
| **滚动特征 (365天)** | `sales_quantity_rolling_mean_365d` | Float | 过去365日平均销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_mean_365d` | Float | 过去365日平均退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_mean_365d`| Float | 过去365日平均净销量 | 历史滚动 |
|
||||||
|
| | `sales_quantity_rolling_sum_365d` | Float | 过去365日总销售量 | 历史滚动 |
|
||||||
|
| | `return_quantity_rolling_sum_365d` | Float | 过去365日总退货量 | 历史滚动 |
|
||||||
|
| | `net_sales_quantity_rolling_sum_365d` | Float | 过去365日总净销量 | 历史滚动 |
|
||||||
|
| **店铺特征** | `province` | String | 店铺所在省份 | 店铺特征 |
|
||||||
|
| | `city` | String | 店铺所在城市 | 店铺特征 |
|
||||||
|
| | `district` | String | 店铺所在行政区 | 店铺特征 |
|
||||||
|
| | `poi_residential_count` | Integer | 周边住宅区POI数量 | 店铺特征 |
|
||||||
|
| | `poi_school_count` | Integer | 周边学校POI数量 | 店铺特征 |
|
||||||
|
| | `poi_mall_count` | Integer | 周边购物中心POI数量 | 店铺特征 |
|
||||||
|
| | `temperature_2m_max` | Float | 当日最高气温 | 店铺特征 |
|
||||||
|
| | `temperature_2m_min` | Float | 当日最低气温 | 店铺特征 |
|
||||||
|
| | `temperature_2m_mean`| Float | 当日平均气温 | 店铺特征 |
|
||||||
|
| **商品特征** | `零售大类代码_encoded` | Integer | 零售大类代码的数字编码 | 商品特征 |
|
||||||
|
| | `零售中类代码_encoded` | Integer | 零售中类代码的数字编码 | 商品特征 |
|
||||||
|
| | `零售小类代码_encoded` | Integer | 零售小类代码的数字编码 | 商品特征 |
|
||||||
|
| | `商品ABC分类_encoded` | Integer | 商品ABC分类的数字编码 | 商品特征 |
|
||||||
|
| | `商品手册代码_encoded` | Integer | 商品手册代码的数字编码 | 商品特征 |
|
||||||
|
| | `产地_encoded` | Integer | 产地的数字编码 | 商品特征 |
|
||||||
|
| | `brand_encoded` | Integer | 品牌的数字编码 | 商品特征 |
|
||||||
|
| | `packaging_quantity` | Float | 包装数量 (从规格中提取) | 商品特征 |
|
||||||
|
| | `approval_type_encoded` | Integer | 批准文号类型的数字编码 | 商品特征 |
|
BIN
data/old_5shops_50skus.parquet
Normal file
BIN
data/old_5shops_50skus.parquet
Normal file
Binary file not shown.
Binary file not shown.
330
server/api.py
330
server/api.py
@ -1398,6 +1398,24 @@ 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
|
||||||
|
|
||||||
|
# 调试步骤:在分析函数调用后,打印其输入和输出
|
||||||
|
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')
|
model_display_name = model_record.get('display_name', 'unknown_model')
|
||||||
safe_model_name = secure_filename(model_display_name).replace(' ', '_').replace('-_', '_')
|
safe_model_name = secure_filename(model_display_name).replace(' ', '_').replace('-_', '_')
|
||||||
@ -1733,12 +1751,25 @@ def get_prediction_history():
|
|||||||
query_params = []
|
query_params = []
|
||||||
|
|
||||||
if product_id:
|
if product_id:
|
||||||
query_conditions.append("json_extract(prediction_scope, '$.product_id') = ?")
|
# v12 修复:支持混合类型的筛选
|
||||||
query_params.append(product_id)
|
# 判断传入的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:
|
if model_type:
|
||||||
query_conditions.append("model_type = ?")
|
# v10 修复: 使用LIKE以兼容(best)版本
|
||||||
query_params.append(model_type)
|
query_conditions.append("model_type LIKE ?")
|
||||||
|
query_params.append(f"{model_type}%")
|
||||||
|
|
||||||
# 构建完整的查询语句
|
# 构建完整的查询语句
|
||||||
query = "SELECT * FROM prediction_history"
|
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):
|
if not full_path or not os.path.exists(full_path):
|
||||||
logger.error(f"预测结果文件不存在或路径无效。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'")
|
logger.error(f"预测结果文件不存在或路径无效。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'")
|
||||||
print(f"DEBUG: JSON文件路径检查失败。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'") # 添加调试打印
|
|
||||||
# 即使文件不存在,也尝试返回基本信息,避免前端崩溃
|
# 即使文件不存在,也尝试返回基本信息,避免前端崩溃
|
||||||
final_payload = {
|
final_payload = {
|
||||||
'product_name': record['product_name'] if 'product_name' in record_keys else 'N/A',
|
'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})
|
return jsonify({"status": "success", "data": final_payload})
|
||||||
|
|
||||||
# 读取和解析JSON文件
|
# 读取和解析JSON文件
|
||||||
print(f"DEBUG: 正在尝试读取JSON文件,绝对路径: '{full_path}'") # 添加调试打印
|
|
||||||
with open(full_path, 'r', encoding='utf-8') as f:
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
saved_data = json.load(f)
|
saved_data = json.load(f)
|
||||||
|
|
||||||
@ -1871,8 +1900,6 @@ def get_prediction_details(prediction_id):
|
|||||||
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', [])
|
||||||
|
|
||||||
# 立即打印提取到的数据长度
|
|
||||||
print(f"DEBUG: Extracted data lengths - History: {len(history_data)}, Prediction: {len(prediction_data)}")
|
|
||||||
|
|
||||||
# 构建最终的、完整的响应数据
|
# 构建最终的、完整的响应数据
|
||||||
final_payload = {
|
final_payload = {
|
||||||
@ -1885,13 +1912,26 @@ def get_prediction_details(prediction_id):
|
|||||||
'analysis': core_data.get('analysis', {}),
|
'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 = {
|
response_data = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": final_payload
|
"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}")
|
logger.info(f"成功构建并返回历史预测详情 (v8): ID={prediction_id}")
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|
||||||
@ -1909,14 +1949,15 @@ def delete_prediction(prediction_id):
|
|||||||
cursor = conn.cursor()
|
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()
|
record = cursor.fetchone()
|
||||||
|
|
||||||
if not record:
|
if not record:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"status": "error", "message": "预测记录不存在"}), 404
|
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,))
|
cursor.execute("DELETE FROM prediction_history WHERE id = ?", (prediction_id,))
|
||||||
@ -1924,8 +1965,19 @@ def delete_prediction(prediction_id):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# 删除预测结果文件
|
# 删除预测结果文件
|
||||||
if os.path.exists(file_path):
|
if relative_file_path:
|
||||||
os.remove(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({
|
return jsonify({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@ -1937,6 +1989,58 @@ def delete_prediction(prediction_id):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({"status": "error", "message": str(e)}), 500
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/history/filter-options', methods=['GET'])
|
||||||
|
def get_history_filter_options():
|
||||||
|
"""获取历史记录页面用于筛选的选项列表"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 从 prediction_history 表中查询所有唯一的 product_name
|
||||||
|
cursor.execute("SELECT DISTINCT product_name FROM prediction_history WHERE product_name IS NOT NULL")
|
||||||
|
records = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 构建选项列表
|
||||||
|
options = [{'value': row['product_name'], 'label': row['product_name']} for row in records]
|
||||||
|
|
||||||
|
return jsonify({"status": "success", "data": options})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取历史筛选选项失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/management/filter-options', methods=['GET'])
|
||||||
|
def get_management_filter_options():
|
||||||
|
"""获取模型管理页面筛选框的动态选项"""
|
||||||
|
try:
|
||||||
|
query = request.args.get('query', '').lower()
|
||||||
|
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# 添加全局预测选项
|
||||||
|
global_option = {'value': 'global', 'label': '全局预测'}
|
||||||
|
if not query or '全局' in query or 'global' in query:
|
||||||
|
options.append(global_option)
|
||||||
|
|
||||||
|
# 获取并筛选产品
|
||||||
|
products = get_available_products()
|
||||||
|
for p in products:
|
||||||
|
label = f"{p.get('product_name', '未知产品')} ({p['product_id']})"
|
||||||
|
if not query or query in label.lower():
|
||||||
|
options.append({'value': p['product_id'], 'label': label})
|
||||||
|
|
||||||
|
# 获取并筛选店铺
|
||||||
|
stores = get_available_stores()
|
||||||
|
for s in stores:
|
||||||
|
label = f"{s.get('store_name', '未知店铺')} ({s['store_id']})"
|
||||||
|
if not query or query in label.lower():
|
||||||
|
options.append({'value': s['store_id'], 'label': label})
|
||||||
|
|
||||||
|
return jsonify({"status": "success", "data": options})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取管理筛选选项失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
# 4. 模型管理API
|
# 4. 模型管理API
|
||||||
@app.route('/api/models', methods=['GET'])
|
@app.route('/api/models', methods=['GET'])
|
||||||
@swag_from({
|
@swag_from({
|
||||||
@ -2143,100 +2247,60 @@ def list_models():
|
|||||||
})
|
})
|
||||||
def get_model_details(model_id):
|
def get_model_details(model_id):
|
||||||
"""
|
"""
|
||||||
获取单个模型的详细信息
|
获取单个模型的详细信息 (v3 - 统一数据结构)
|
||||||
---
|
|
||||||
tags:
|
|
||||||
- 模型管理
|
|
||||||
parameters:
|
|
||||||
- name: model_id
|
|
||||||
in: path
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
description: "模型的唯一标识符 (格式: model_type_product_id)"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: 模型的详细信息
|
|
||||||
404:
|
|
||||||
description: 未找到模型
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
model_type, product_id = model_id.split('_', 1)
|
# 智能处理带 '_best' 后缀的UID
|
||||||
|
db_query_uid = model_id[:-len('_best')] if model_id.endswith('_best') else model_id
|
||||||
|
|
||||||
# 处理优化版KAN模型的文件名
|
model_record = find_model_by_uid(db_query_uid)
|
||||||
file_model_type = model_type
|
|
||||||
if model_type == 'optimized_kan':
|
|
||||||
file_model_type = 'kan_optimized'
|
|
||||||
|
|
||||||
# 首先尝试从app配置中获取模型目录
|
if not model_record:
|
||||||
models_dir = app.config.get('MODEL_DIR', DEFAULT_MODEL_DIR)
|
return jsonify({"status": "error", "message": "模型未找到"}), 404
|
||||||
|
|
||||||
# 检查models_dir是否存在,如果不存在,使用DEFAULT_MODEL_DIR作为后备
|
# 将 sqlite3.Row 转换为可修改的字典
|
||||||
if not os.path.exists(models_dir) and os.path.exists(DEFAULT_MODEL_DIR):
|
model_data = dict(model_record)
|
||||||
print(f"警告: 配置的模型目录 '{models_dir}' 不存在,使用默认目录 '{DEFAULT_MODEL_DIR}'")
|
|
||||||
models_dir = DEFAULT_MODEL_DIR
|
|
||||||
|
|
||||||
# 尝试多种可能的文件名格式
|
# 解析JSON字段
|
||||||
possible_patterns = [
|
model_data['training_scope'] = json.loads(model_data.get('training_scope', '{}'))
|
||||||
f'{file_model_type}_product_{product_id}_v1.pth', # 新格式
|
model_data['performance_metrics'] = json.loads(model_data.get('performance_metrics', '{}'))
|
||||||
f'{file_model_type}_model_product_{product_id}.pth', # 旧格式
|
model_data['artifacts'] = json.loads(model_data.get('artifacts', '{}'))
|
||||||
f'{file_model_type}_{product_id}_v1.pth', # 备用格式
|
|
||||||
]
|
|
||||||
|
|
||||||
model_path = None
|
# 统一化修复:完全复制 list_models 的名称处理逻辑,确保数据结构一致
|
||||||
for pattern in possible_patterns:
|
scope = model_data.get('training_scope', {})
|
||||||
test_path = os.path.join(models_dir, pattern)
|
mode = model_data.get('training_mode')
|
||||||
if os.path.exists(test_path):
|
|
||||||
model_path = test_path
|
|
||||||
print(f"找到模型文件: {pattern}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not model_path:
|
# 初始化所有可能的名称字段
|
||||||
print(f"未找到模型文件,尝试的路径:")
|
model_data['product_name'] = None
|
||||||
for pattern in possible_patterns:
|
model_data['store_name'] = None
|
||||||
test_path = os.path.join(models_dir, pattern)
|
# 优先使用数据库中的 display_name
|
||||||
print(f" - {test_path}")
|
display_name = model_data.get('display_name')
|
||||||
return jsonify({"status": "error", "error": "模型未找到"}), 404
|
|
||||||
|
|
||||||
# 加载模型文件
|
if isinstance(scope, dict):
|
||||||
try:
|
product_info = scope.get('product')
|
||||||
# 添加weights_only=False参数,解决PyTorch 2.6序列化问题
|
store_info = scope.get('store')
|
||||||
checkpoint = torch.load(model_path, map_location='cpu', weights_only=False)
|
|
||||||
|
|
||||||
# 提取模型信息
|
if mode == 'product' and isinstance(product_info, dict):
|
||||||
model_info = {
|
model_data['product_name'] = product_info.get('name')
|
||||||
"model_id": model_id,
|
if not display_name: display_name = model_data['product_name']
|
||||||
"product_id": product_id,
|
|
||||||
"model_type": model_type,
|
|
||||||
"created_at": datetime.fromtimestamp(os.path.getctime(model_path)).isoformat(),
|
|
||||||
"file_path": model_path,
|
|
||||||
"file_size": f"{os.path.getsize(model_path) / (1024 * 1024):.2f} MB"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 如果checkpoint是字典,提取其中的信息
|
elif mode == 'store' and isinstance(store_info, dict):
|
||||||
if isinstance(checkpoint, dict):
|
model_data['store_name'] = store_info.get('name')
|
||||||
# 提取配置信息
|
if not display_name: display_name = model_data['store_name']
|
||||||
if 'config' in checkpoint:
|
|
||||||
config = checkpoint['config']
|
|
||||||
for key, value in config.items():
|
|
||||||
model_info[key] = value
|
|
||||||
|
|
||||||
# 提取评估指标
|
elif mode == 'global':
|
||||||
if 'metrics' in checkpoint:
|
if not display_name: display_name = "全局模型"
|
||||||
model_info['metrics'] = checkpoint['metrics']
|
|
||||||
|
|
||||||
# 获取产品名称
|
# 提供最终的后备方案
|
||||||
product_name = get_product_name(product_id)
|
if not display_name:
|
||||||
if product_name:
|
display_name = "信息不完整"
|
||||||
model_info['product_name'] = product_name
|
|
||||||
|
|
||||||
return jsonify({"status": "success", "data": model_info})
|
model_data['display_name'] = display_name
|
||||||
except Exception as e:
|
|
||||||
print(f"加载模型文件失败: {str(e)}")
|
return jsonify({"status": "success", "data": model_data})
|
||||||
return jsonify({"status": "error", "error": f"加载模型文件失败: {str(e)}"}), 500
|
|
||||||
except ValueError:
|
|
||||||
return jsonify({"status": "error", "error": "无效的model_id格式"}), 400
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"status": "error", "error": f"获取模型详情失败: {e}"}), 500
|
logger.error(f"获取模型详情失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/models/<model_id>', methods=['DELETE'])
|
@app.route('/api/models/<model_id>', methods=['DELETE'])
|
||||||
@swag_from({
|
@swag_from({
|
||||||
@ -2273,68 +2337,46 @@ def get_model_details(model_id):
|
|||||||
})
|
})
|
||||||
def delete_model(model_id):
|
def delete_model(model_id):
|
||||||
"""
|
"""
|
||||||
删除一个模型及其关联文件
|
删除一个模型及其关联文件 (v2 - 基于数据库)
|
||||||
---
|
|
||||||
tags:
|
|
||||||
- 模型管理
|
|
||||||
parameters:
|
|
||||||
- name: model_id
|
|
||||||
in: path
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
description: "要删除的模型的ID (格式: model_type_product_id)"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: 模型删除成功
|
|
||||||
404:
|
|
||||||
description: 模型未找到
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
model_type, product_id = model_id.split('_', 1)
|
# 智能处理带 '_best' 后缀的UID
|
||||||
|
db_query_uid = model_id[:-len('_best')] if model_id.endswith('_best') else model_id
|
||||||
|
|
||||||
# 处理优化版KAN模型的文件名
|
conn = get_db_connection()
|
||||||
file_model_type = model_type
|
cursor = conn.cursor()
|
||||||
if model_type == 'optimized_kan':
|
|
||||||
file_model_type = 'kan_optimized'
|
|
||||||
|
|
||||||
# 首先尝试从app配置中获取模型目录
|
# 查找模型记录
|
||||||
models_dir = app.config.get('MODEL_DIR', DEFAULT_MODEL_DIR)
|
cursor.execute("SELECT artifacts FROM models WHERE model_uid = ?", (db_query_uid,))
|
||||||
|
record = cursor.fetchone()
|
||||||
|
|
||||||
# 检查models_dir是否存在,如果不存在,使用DEFAULT_MODEL_DIR作为后备
|
if not record:
|
||||||
if not os.path.exists(models_dir) and os.path.exists(DEFAULT_MODEL_DIR):
|
conn.close()
|
||||||
print(f"警告: 配置的模型目录 '{models_dir}' 不存在,使用默认目录 '{DEFAULT_MODEL_DIR}'")
|
return jsonify({"status": "error", "message": "模型未找到"}), 404
|
||||||
models_dir = DEFAULT_MODEL_DIR
|
|
||||||
|
|
||||||
# 尝试多种可能的文件名格式
|
# 删除数据库记录
|
||||||
possible_patterns = [
|
cursor.execute("DELETE FROM models WHERE model_uid = ?", (db_query_uid,))
|
||||||
f'{file_model_type}_product_{product_id}_v1.pth', # 新格式
|
conn.commit()
|
||||||
f'{file_model_type}_model_product_{product_id}.pth', # 旧格式
|
|
||||||
f'{file_model_type}_{product_id}_v1.pth', # 备用格式
|
|
||||||
]
|
|
||||||
|
|
||||||
model_path = None
|
# 删除关联的模型文件
|
||||||
for pattern in possible_patterns:
|
try:
|
||||||
test_path = os.path.join(models_dir, pattern)
|
artifacts = json.loads(record['artifacts'])
|
||||||
if os.path.exists(test_path):
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
model_path = test_path
|
|
||||||
print(f"找到模型文件: {pattern}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not model_path:
|
for key, path in artifacts.items():
|
||||||
print(f"未找到模型文件,尝试的路径:")
|
if path and isinstance(path, str):
|
||||||
for pattern in possible_patterns:
|
full_path = os.path.join(project_root, path)
|
||||||
test_path = os.path.join(models_dir, pattern)
|
if os.path.exists(full_path):
|
||||||
print(f" - {test_path}")
|
os.remove(full_path)
|
||||||
return jsonify({"status": "error", "error": "模型未找到"}), 404
|
logger.info(f"已删除文件: {full_path}")
|
||||||
|
except (json.JSONDecodeError, TypeError, OSError) as e:
|
||||||
# 删除模型文件
|
logger.error(f"删除模型文件失败: {e}")
|
||||||
os.remove(model_path)
|
|
||||||
|
|
||||||
|
conn.close()
|
||||||
return jsonify({"status": "success", "message": f"模型 {model_id} 已删除"})
|
return jsonify({"status": "success", "message": f"模型 {model_id} 已删除"})
|
||||||
except ValueError:
|
|
||||||
return jsonify({"status": "error", "error": "无效的model_id格式"}), 400
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"status": "error", "error": f"删除模型失败: {e}"}), 500
|
logger.error(f"删除模型失败: {e}\n{traceback.format_exc()}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/models/<model_id>/export', methods=['GET'])
|
@app.route('/api/models/<model_id>/export', methods=['GET'])
|
||||||
@swag_from({
|
@swag_from({
|
||||||
|
@ -65,13 +65,21 @@ def query_models_from_db(filters: dict, page: int = 1, page_size: int = 10):
|
|||||||
conditions = []
|
conditions = []
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if filters.get('product_id'):
|
product_id_filter = filters.get('product_id')
|
||||||
conditions.append("json_extract(training_scope, '$.product.id') = ?")
|
if product_id_filter:
|
||||||
params.append(filters['product_id'])
|
if product_id_filter.lower() == 'global':
|
||||||
|
conditions.append("training_mode = ?")
|
||||||
|
params.append('global')
|
||||||
|
elif product_id_filter.startswith('S'):
|
||||||
|
conditions.append("json_extract(training_scope, '$.store.id') = ?")
|
||||||
|
params.append(product_id_filter)
|
||||||
|
else:
|
||||||
|
conditions.append("(json_extract(training_scope, '$.product.id') = ? OR display_name LIKE ?)")
|
||||||
|
params.extend([product_id_filter, f"%{product_id_filter}%"])
|
||||||
|
|
||||||
if filters.get('model_type'):
|
if filters.get('model_type'):
|
||||||
conditions.append("model_type = ?")
|
conditions.append("model_type LIKE ?")
|
||||||
params.append(filters['model_type'])
|
params.append(f"{filters['model_type']}%")
|
||||||
|
|
||||||
if filters.get('store_id'):
|
if filters.get('store_id'):
|
||||||
conditions.append("json_extract(training_scope, '$.store.id') = ?")
|
conditions.append("json_extract(training_scope, '$.store.id') = ?")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user