Compare commits
5 Commits
6c82cf001b
...
cc30295f1d
Author | SHA1 | Date | |
---|---|---|---|
cc30295f1d | |||
066a0429e5 | |||
6c11aff234 | |||
b1b697117b | |||
cfb50d0573 |
@ -17,6 +17,7 @@
|
||||
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:default-openeds="['1']"
|
||||
router
|
||||
class="futuristic-menu"
|
||||
@ -31,8 +32,8 @@
|
||||
<el-menu-item index="/">
|
||||
<el-icon><House /></el-icon>首页概览
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/data">
|
||||
<el-icon><FolderOpened /></el-icon>数据管理
|
||||
<el-menu-item index="/store-management">
|
||||
<el-icon><Shop /></el-icon>店铺管理
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="training-submenu">
|
||||
<template #title>
|
||||
@ -70,9 +71,6 @@
|
||||
<el-menu-item index="/management">
|
||||
<el-icon><Files /></el-icon>模型管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/store-management">
|
||||
<el-icon><Shop /></el-icon>店铺管理
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
@ -112,7 +110,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { DataAnalysis, Refresh, DataLine, House, FolderOpened, Cpu, MagicStick, Files, Histogram, Coin, Shop, Operation } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const activeMenu = computed(() => route.path)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -9,11 +9,6 @@ const router = createRouter({
|
||||
name: 'dashboard',
|
||||
component: DashboardView
|
||||
},
|
||||
{
|
||||
path: '/data',
|
||||
name: 'data',
|
||||
component: () => import('../views/DataView.vue')
|
||||
},
|
||||
{
|
||||
path: '/training',
|
||||
name: 'training',
|
||||
|
@ -1,461 +0,0 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>销售数据管理</span>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
>
|
||||
<el-button type="primary">上传销售数据</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 查询过滤条件 -->
|
||||
<div class="filter-section">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-select v-model="filters.store_id" placeholder="选择店铺" clearable @change="handleFilterChange">
|
||||
<el-option label="全部店铺" value=""></el-option>
|
||||
<el-option
|
||||
v-for="store in stores"
|
||||
:key="store.store_id"
|
||||
:label="store.store_name"
|
||||
:value="store.store_id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-select v-model="filters.product_id" placeholder="选择产品" clearable @change="handleFilterChange">
|
||||
<el-option label="全部产品" value=""></el-option>
|
||||
<el-option
|
||||
v-for="product in allProducts"
|
||||
:key="product.product_id"
|
||||
:label="product.product_name"
|
||||
:value="product.product_id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" @click="handleFilterChange">查询</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 销售数据表格 -->
|
||||
<el-table :data="salesData" stripe v-loading="loading" class="mt-4">
|
||||
<el-table-column prop="date" label="日期" width="120"></el-table-column>
|
||||
<el-table-column prop="store_name" label="店铺名称" width="150"></el-table-column>
|
||||
<el-table-column prop="store_id" label="店铺ID" width="100"></el-table-column>
|
||||
<el-table-column prop="product_name" label="产品名称" width="150"></el-table-column>
|
||||
<el-table-column prop="product_id" label="产品ID" width="100"></el-table-column>
|
||||
<el-table-column prop="quantity_sold" label="销量" width="80" align="right"></el-table-column>
|
||||
<el-table-column prop="unit_price" label="单价" width="80" align="right">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.unit_price?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sales_amount" label="销售额" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.sales_amount?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="store_type" label="店铺类型" width="100"></el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button link @click="viewStoreDetails(row.store_id)">店铺详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="statistics-section mt-4" v-if="statistics">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总记录数" :value="statistics.total_records" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总销售额" :value="statistics.total_sales_amount" :precision="2" prefix="¥" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总销量" :value="statistics.total_quantity" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="店铺数量" :value="statistics.stores" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 产品详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="`${selectedProduct?.product_name} - 销售详情`"
|
||||
width="60%"
|
||||
>
|
||||
<div v-loading="detailLoading">
|
||||
<div v-if="salesData.length > 0">
|
||||
<div class="chart-container">
|
||||
<canvas ref="salesChartCanvas"></canvas>
|
||||
</div>
|
||||
<el-table :data="paginatedSalesData" stripe>
|
||||
<el-table-column prop="date" label="日期"></el-table-column>
|
||||
<el-table-column prop="sales" label="销量"></el-table-column>
|
||||
<el-table-column prop="price" label="价格"></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
layout="prev, pager, next"
|
||||
:total="salesData.length"
|
||||
:page-size="pageSize"
|
||||
@current-change="handlePageChange"
|
||||
class="mt-4"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="暂无销售数据"></el-empty>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Chart from 'chart.js/auto';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
|
||||
Chart.register(zoomPlugin);
|
||||
|
||||
// 数据相关
|
||||
const stores = ref([])
|
||||
const allProducts = ref([])
|
||||
const salesData = ref([])
|
||||
const statistics = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
// 分页相关
|
||||
const pageSize = ref(20)
|
||||
const currentPage = ref(1)
|
||||
const total = ref(0)
|
||||
|
||||
// 过滤条件
|
||||
const filters = ref({
|
||||
store_id: '',
|
||||
product_id: '',
|
||||
dateRange: null
|
||||
})
|
||||
|
||||
// 对话框相关
|
||||
const dialogVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const selectedProduct = ref(null)
|
||||
const paginatedSalesData = ref([])
|
||||
const salesChartCanvas = ref(null)
|
||||
let salesChart = null;
|
||||
|
||||
// 获取店铺列表
|
||||
const fetchStores = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/stores')
|
||||
if (response.data.status === 'success') {
|
||||
stores.value = response.data.data
|
||||
} else {
|
||||
ElMessage.error('获取店铺列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取产品列表
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/products')
|
||||
if (response.data.status === 'success') {
|
||||
allProducts.value = response.data.data
|
||||
} else {
|
||||
ElMessage.error('获取产品列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取销售数据
|
||||
const fetchSalesData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 构建查询参数
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
|
||||
if (filters.value.store_id) {
|
||||
params.store_id = filters.value.store_id
|
||||
}
|
||||
|
||||
if (filters.value.product_id) {
|
||||
params.product_id = filters.value.product_id
|
||||
}
|
||||
|
||||
if (filters.value.dateRange && filters.value.dateRange.length === 2) {
|
||||
params.start_date = filters.value.dateRange[0]
|
||||
params.end_date = filters.value.dateRange[1]
|
||||
}
|
||||
|
||||
const response = await axios.get('/api/sales/data', { params })
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
salesData.value = response.data.data
|
||||
total.value = response.data.total || 0
|
||||
statistics.value = response.data.statistics
|
||||
} else {
|
||||
ElMessage.error('获取销售数据失败')
|
||||
salesData.value = []
|
||||
total.value = 0
|
||||
statistics.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请求销售数据时出错')
|
||||
console.error(error)
|
||||
salesData.value = []
|
||||
total.value = 0
|
||||
statistics.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理过滤条件变化
|
||||
const handleFilterChange = () => {
|
||||
currentPage.value = 1
|
||||
fetchSalesData()
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchSalesData()
|
||||
}
|
||||
|
||||
// 处理页面大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchSalesData()
|
||||
}
|
||||
|
||||
// 查看店铺详情
|
||||
const viewStoreDetails = async (storeId) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/stores/${storeId}`)
|
||||
if (response.data.status === 'success') {
|
||||
const store = response.data.data
|
||||
ElMessage.info(`店铺:${store.store_name},位置:${store.location},类型:${store.type}`)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取店铺详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
const handleUpload = async (options) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', options.file)
|
||||
try {
|
||||
const response = await axios.post('/api/data/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
if (response.data.status === 'success') {
|
||||
ElMessage.success('数据上传成功')
|
||||
await fetchStores()
|
||||
await fetchProducts()
|
||||
await fetchSalesData()
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '数据上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('数据上传请求失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetails = async (product) => {
|
||||
selectedProduct.value = product;
|
||||
dialogVisible.value = true;
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const response = await axios.get(`/api/products/${product.product_id}/sales`);
|
||||
if (response.data.status === 'success') {
|
||||
salesData.value = response.data.data;
|
||||
handlePageChange(1); // Show first page
|
||||
await nextTick();
|
||||
renderChart();
|
||||
} else {
|
||||
ElMessage.error('获取销售详情失败');
|
||||
salesData.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请求销售详情时出错');
|
||||
salesData.value = [];
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// handlePageChange函数已在上面定义
|
||||
|
||||
const renderChart = () => {
|
||||
if (salesChart) {
|
||||
salesChart.destroy();
|
||||
}
|
||||
if (!salesChartCanvas.value || salesData.value.length === 0) return;
|
||||
|
||||
const labels = salesData.value.map(d => d.date);
|
||||
const data = salesData.value.map(d => d.sales);
|
||||
|
||||
salesChart = new Chart(salesChartCanvas.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: '每日销量',
|
||||
data,
|
||||
borderColor: '#409EFF',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: function(context) {
|
||||
return `日期: ${context[0].label}`;
|
||||
},
|
||||
label: (context) => {
|
||||
const label = context.dataset.label || '';
|
||||
const value = context.parsed.y;
|
||||
const fullData = salesData.value[context.dataIndex];
|
||||
let tooltipText = `${label}: ${value}`;
|
||||
if (fullData) {
|
||||
tooltipText += ` | 温度: ${fullData.temperature}°C`;
|
||||
}
|
||||
return tooltipText;
|
||||
}
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'x',
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true
|
||||
},
|
||||
mode: 'x',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载时初始化数据
|
||||
onMounted(async () => {
|
||||
await fetchStores()
|
||||
await fetchProducts()
|
||||
await fetchSalesData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.statistics-section {
|
||||
padding: 20px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0f2fe;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-statistic {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-table .el-table__cell {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.filter-section .el-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-section .el-col {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-section .el-col {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.statistics-section .el-col {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
101
feature_branch_workflow.md
Normal file
101
feature_branch_workflow.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 功能分支开发与合并标准流程
|
||||
|
||||
本文档旨在说明一个标准、安全的功能开发流程,涵盖从创建分支到最终合并的完整步骤。
|
||||
|
||||
## 流程概述
|
||||
|
||||
1. **创建功能分支**:基于主开发分支(如 `lyf-dev`)在远程仓库创建一个新的功能分支(如 `lyf-dev-req0001`)。
|
||||
2. **同步到本地**:将远程的新分支同步到本地,并切换到该分支进行开发。
|
||||
3. **开发与提交**:在功能分支上进行代码开发,并频繁提交改动。
|
||||
4. **推送到远程**:定期将本地的提交推送到远程功能分支,用于备份和协作。
|
||||
5. **合并回主分支**:当功能开发和测试完成后,将功能分支合并回主开发分支。
|
||||
|
||||
---
|
||||
|
||||
## 详细操作步骤
|
||||
|
||||
### 第一步:同步并切换到功能分支
|
||||
|
||||
当远程仓库已经创建了新的功能分支后(例如 `lyf-dev-req0001`),本地需要执行以下命令来同步和切换。
|
||||
|
||||
1. **获取远程最新信息**:
|
||||
```bash
|
||||
git fetch
|
||||
```
|
||||
这个命令会拉取远程仓库的所有最新信息,包括新建的分支。
|
||||
|
||||
2. **创建并切换到本地分支**:
|
||||
```bash
|
||||
git checkout lyf-dev-req0001
|
||||
```
|
||||
Git 会自动检测到远程存在一个同名分支,并为您创建一个本地分支来跟踪它。
|
||||
|
||||
### 第二步:在功能分支上开发和提交
|
||||
|
||||
现在您可以在 `lyf-dev-req0001` 分支上安全地进行开发。
|
||||
|
||||
1. **进行代码修改**:添加、修改或删除文件以实现新功能。
|
||||
|
||||
2. **提交代码改动**:
|
||||
```bash
|
||||
# 添加所有修改过的文件到暂存区
|
||||
git add .
|
||||
|
||||
# 提交改动到本地仓库,并附上有意义的说明
|
||||
git commit -m "feat: 完成用户认证模块"
|
||||
```
|
||||
> **最佳实践**:保持提交的粒度小且描述清晰,方便代码审查和问题回溯。
|
||||
|
||||
### 第三步:推送功能分支到远程
|
||||
|
||||
为了备份代码和进行团队协作,需要将本地的提交推送到远程仓库。
|
||||
|
||||
```bash
|
||||
# 将当前分支 (lyf-dev-req0001) 的提交推送到远程同名分支
|
||||
git push origin lyf-dev-req0001
|
||||
```
|
||||
|
||||
### 第四步:合并功能到主开发分支 (`lyf-dev`)
|
||||
|
||||
当功能开发完毕并通过测试后,就可以准备将其合并回 `lyf-dev` 分支。
|
||||
|
||||
1. **切换到主开发分支**:
|
||||
```bash
|
||||
git checkout lyf-dev
|
||||
```
|
||||
|
||||
2. **确保主开发分支是最新版本**:
|
||||
在合并前,务必先拉取远程 `lyf-dev` 的最新代码,以减少冲突的可能性。
|
||||
```bash
|
||||
git pull origin lyf-dev
|
||||
```
|
||||
|
||||
3. **合并功能分支**:
|
||||
将 `lyf-dev-req0001` 的所有改动合并到当前的 `lyf-dev` 分支。
|
||||
```bash
|
||||
git merge lyf-dev-req0001
|
||||
```
|
||||
* **如果出现冲突 (Conflict)**:Git 会提示您哪些文件存在冲突。您需要手动打开这些文件,解决冲突部分,然后再次执行 `git add .` 和 `git commit` 来完成合并提交。
|
||||
* **如果没有冲突**:Git 会自动创建一个合并提交。
|
||||
|
||||
4. **将合并后的主分支推送到远程**:
|
||||
```bash
|
||||
git push origin lyf-dev
|
||||
```
|
||||
|
||||
### 第五步:清理(可选)
|
||||
|
||||
当功能分支确认不再需要后,可以删除它以保持仓库整洁。
|
||||
|
||||
1. **删除远程分支**:
|
||||
```bash
|
||||
git push origin --delete lyf-dev-req0001
|
||||
```
|
||||
|
||||
2. **删除本地分支**:
|
||||
```bash
|
||||
git branch -d lyf-dev-req0001
|
||||
```
|
||||
|
||||
---
|
||||
遵循以上流程可以确保您的开发工作流程清晰、安全且高效。
|
@ -56,3 +56,5 @@ tzdata==2025.2
|
||||
werkzeug==3.1.3
|
||||
win32-setctime==1.2.0
|
||||
wsproto==1.2.0
|
||||
|
||||
python-dateutil
|
||||
|
@ -10,6 +10,13 @@ import os
|
||||
import re
|
||||
import glob
|
||||
|
||||
# 项目根目录
|
||||
# __file__ 是当前文件 (config.py) 的路径
|
||||
# os.path.dirname(__file__) 是 server/core
|
||||
# os.path.join(..., '..') 是 server
|
||||
# os.path.join(..., '..', '..') 是项目根目录
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# 解决画图中文显示问题
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
@ -26,8 +33,9 @@ def get_device():
|
||||
DEVICE = get_device()
|
||||
|
||||
# 数据相关配置
|
||||
DEFAULT_DATA_PATH = 'pharmacy_sales.xlsx'
|
||||
DEFAULT_MODEL_DIR = 'saved_models'
|
||||
# 使用 os.path.join 构造跨平台的路径
|
||||
DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
|
||||
DEFAULT_MODEL_DIR = os.path.join(PROJECT_ROOT, 'saved_models')
|
||||
DEFAULT_FEATURES = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature']
|
||||
|
||||
# 时间序列参数
|
||||
|
@ -41,7 +41,7 @@ class PharmacyPredictor:
|
||||
"""
|
||||
# 设置默认数据路径为多店铺CSV文件
|
||||
if data_path is None:
|
||||
data_path = 'data/timeseries_training_data_sample_10s50p.parquet'
|
||||
data_path = DEFAULT_DATA_PATH
|
||||
|
||||
self.data_path = data_path
|
||||
self.model_dir = model_dir
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -340,7 +340,7 @@ def train_product_model_with_mlstm(
|
||||
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=patience // 2, factor=0.5, verbose=True)
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=patience // 2, factor=0.5)
|
||||
|
||||
emit_progress("数据预处理完成,开始模型训练...", progress=10)
|
||||
|
||||
|
@ -279,7 +279,7 @@ def train_product_model_with_transformer(
|
||||
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=patience // 2, factor=0.5, verbose=True)
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=patience // 2, factor=0.5)
|
||||
|
||||
# 训练模型
|
||||
train_losses = []
|
||||
|
@ -8,8 +8,9 @@ import numpy as np
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Tuple, Dict, Any
|
||||
from core.config import DEFAULT_DATA_PATH
|
||||
|
||||
def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
|
||||
def load_multi_store_data(file_path: str = None,
|
||||
store_id: Optional[str] = None,
|
||||
product_id: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
@ -18,7 +19,7 @@ def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample
|
||||
加载多店铺销售数据,支持按店铺、产品、时间范围过滤
|
||||
|
||||
参数:
|
||||
file_path: 数据文件路径 (支持 .csv, .xlsx, .parquet)
|
||||
file_path: 数据文件路径 (支持 .csv, .xlsx, .parquet)。如果为None,则使用config中定义的默认路径。
|
||||
store_id: 店铺ID,为None时返回所有店铺数据
|
||||
product_id: 产品ID,为None时返回所有产品数据
|
||||
start_date: 开始日期 (YYYY-MM-DD)
|
||||
@ -28,43 +29,27 @@ def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample
|
||||
DataFrame: 过滤后的销售数据
|
||||
"""
|
||||
|
||||
# 尝试多个可能的文件路径
|
||||
# 获取当前脚本所在的目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 假设项目根目录是 server/utils 的上两级目录
|
||||
project_root = os.path.abspath(os.path.join(current_dir, '..', '..'))
|
||||
# 如果未提供文件路径,则使用配置文件中的默认路径
|
||||
if file_path is None:
|
||||
file_path = DEFAULT_DATA_PATH
|
||||
|
||||
possible_paths = [
|
||||
file_path, # 相对路径 (如果从根目录运行)
|
||||
os.path.join(project_root, file_path), # 基于项目根目录的绝对路径
|
||||
os.path.join('..', file_path), # 相对路径 (如果从 server 目录运行)
|
||||
os.path.join('server', file_path) # 相对路径 (如果从根目录运行,但路径错误)
|
||||
]
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"数据文件不存在: {file_path}")
|
||||
|
||||
df = None
|
||||
loaded_path = None
|
||||
for path in possible_paths:
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
try:
|
||||
if file_path.endswith('.csv'):
|
||||
df = pd.read_csv(file_path)
|
||||
elif file_path.endswith('.xlsx'):
|
||||
df = pd.read_excel(file_path)
|
||||
elif file_path.endswith('.parquet'):
|
||||
df = pd.read_parquet(file_path)
|
||||
else:
|
||||
raise ValueError(f"不支持的文件格式: {file_path}")
|
||||
|
||||
if path.endswith('.csv'):
|
||||
df = pd.read_csv(path)
|
||||
elif path.endswith('.xlsx'):
|
||||
df = pd.read_excel(path)
|
||||
elif path.endswith('.parquet'):
|
||||
df = pd.read_parquet(path)
|
||||
|
||||
if df is not None:
|
||||
loaded_path = path
|
||||
print(f"成功加载数据文件: {loaded_path}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"加载文件 {path} 失败: {e}")
|
||||
continue
|
||||
|
||||
if df is None:
|
||||
raise FileNotFoundError(f"无法找到或加载数据文件,尝试的路径: {possible_paths}")
|
||||
print(f"成功加载数据文件: {file_path}")
|
||||
except Exception as e:
|
||||
print(f"加载文件 {file_path} 失败: {e}")
|
||||
raise
|
||||
|
||||
# 按店铺过滤
|
||||
if store_id:
|
||||
@ -192,7 +177,7 @@ def standardize_column_names(df: pd.DataFrame) -> pd.DataFrame:
|
||||
|
||||
return df
|
||||
|
||||
def get_available_stores(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> List[Dict[str, Any]]:
|
||||
def get_available_stores(file_path: str = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取可用的店铺列表
|
||||
|
||||
@ -229,7 +214,7 @@ def get_available_stores(file_path: str = 'data/timeseries_training_data_sample_
|
||||
print(f"获取店铺列表失败: {e}")
|
||||
return []
|
||||
|
||||
def get_available_products(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
|
||||
def get_available_products(file_path: str = None,
|
||||
store_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取可用的产品列表
|
||||
@ -260,7 +245,7 @@ def get_available_products(file_path: str = 'data/timeseries_training_data_sampl
|
||||
|
||||
def get_store_product_sales_data(store_id: str,
|
||||
product_id: str,
|
||||
file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> pd.DataFrame:
|
||||
file_path: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取特定店铺和产品的销售数据,用于模型训练
|
||||
|
||||
@ -305,7 +290,7 @@ def get_store_product_sales_data(store_id: str,
|
||||
def aggregate_multi_store_data(product_id: Optional[str] = None,
|
||||
store_id: Optional[str] = None,
|
||||
aggregation_method: str = 'sum',
|
||||
file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> pd.DataFrame:
|
||||
file_path: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
聚合销售数据,可按产品(全局)或按店铺(所有产品)
|
||||
|
||||
@ -393,7 +378,7 @@ def aggregate_multi_store_data(product_id: Optional[str] = None,
|
||||
# 返回只包含这些必需列的DataFrame
|
||||
return aggregated_df[existing_columns]
|
||||
|
||||
def get_sales_statistics(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
|
||||
def get_sales_statistics(file_path: str = None,
|
||||
store_id: Optional[str] = None,
|
||||
product_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
@ -432,7 +417,7 @@ def get_sales_statistics(file_path: str = 'data/timeseries_training_data_sample_
|
||||
return {'error': str(e)}
|
||||
|
||||
# 向后兼容的函数
|
||||
def load_data(file_path='pharmacy_sales.xlsx', store_id=None):
|
||||
def load_data(file_path=None, store_id=None):
|
||||
"""
|
||||
向后兼容的数据加载函数
|
||||
"""
|
||||
|
@ -1,3 +1,11 @@
|
||||
### 根目录启动
|
||||
`uv pip install loguru numpy pandas torch matplotlib flask flask_cors flask_socketio flasgger scikit-learn tqdm pytorch_tcn`
|
||||
|
||||
### UI
|
||||
`npm install` `npm run dev`
|
||||
|
||||
|
||||
|
||||
# “预测分析”模块UI重构修改记录
|
||||
|
||||
**任务目标**: 将原有的、通过下拉菜单切换模式的单一预测页面,重构为通过左侧子导航切换模式的多页面布局,使其UI结构与“模型训练”模块保持一致。
|
||||
@ -636,4 +644,115 @@
|
||||
|
||||
|
||||
|
||||
---
|
||||
**日期**: 2025-07-14
|
||||
**主题**: UI导航栏重构
|
||||
|
||||
### 描述
|
||||
根据用户请求,对左侧功能导航栏进行了调整。
|
||||
|
||||
### 主要改动
|
||||
1. **删除“数据管理”**:
|
||||
* 从 `UI/src/App.vue` 的导航菜单中移除了“数据管理”项。
|
||||
* 从 `UI/src/router/index.js` 中删除了对应的 `/data` 路由。
|
||||
* 删除了视图文件 `UI/src/views/DataView.vue`。
|
||||
|
||||
2. **提升“店铺管理”**:
|
||||
* 将“店铺管理”菜单项在 `UI/src/App.vue` 中的位置提升,以填补原“数据管理”的位置,使其在导航中更加突出。
|
||||
|
||||
### 涉及文件
|
||||
* `UI/src/App.vue`
|
||||
* `UI/src/router/index.js`
|
||||
* `UI/src/views/DataView.vue` (已删除)
|
||||
|
||||
|
||||
|
||||
|
||||
**按药品模型预测**
|
||||
---
|
||||
**日期**: 2025-07-14
|
||||
**主题**: 修复导航菜单高亮问题
|
||||
|
||||
### 描述
|
||||
修复了首次进入或刷新页面时,左侧导航菜单项与当前路由不匹配导致不高亮的问题。
|
||||
|
||||
### 主要改动
|
||||
* **文件**: `UI/src/App.vue`
|
||||
* **修改**:
|
||||
1. 引入 `useRoute` 和 `computed`。
|
||||
2. 创建了一个计算属性 `activeMenu`,其值动态地等于当前路由的路径 (`route.path`)。
|
||||
3. 将 `el-menu` 组件的 `:default-active` 属性绑定到 `activeMenu`。
|
||||
|
||||
### 结果
|
||||
确保了导航菜单的高亮状态始终与当前页面的URL保持同步。
|
||||
|
||||
---
|
||||
**日期**: 2025-07-15
|
||||
**主题**: 修复硬编码文件路径问题,提高项目可移植性
|
||||
|
||||
### 问题描述
|
||||
项目在从一台计算机迁移到另一台时,由于数据文件路径被硬编码在代码中,导致程序无法找到数据文件而运行失败。
|
||||
|
||||
### 根本原因
|
||||
多个Python文件(`predictor.py`, `multi_store_data_utils.py`)中直接写入了相对路径 `'data/timeseries_training_data_sample_10s50p.parquet'` 作为默认值。这种方式在不同运行环境下(如从根目录运行 vs 从子目录运行)会产生路径解析错误。
|
||||
|
||||
### 解决方案:集中配置,统一管理
|
||||
1. **修改 `server/core/config.py` (核心)**:
|
||||
* 动态计算并定义了一个全局变量 `PROJECT_ROOT`,它始终指向项目的根目录。
|
||||
* 基于 `PROJECT_ROOT`,使用 `os.path.join` 创建了一个跨平台的、绝对的默认数据路径 `DEFAULT_DATA_PATH` 和模型保存路径 `DEFAULT_MODEL_DIR`。
|
||||
* 这确保了无论从哪个位置执行代码,路径总能被正确解析。
|
||||
|
||||
2. **修改 `server/utils/multi_store_data_utils.py`**:
|
||||
* 从 `server/core/config` 导入 `DEFAULT_DATA_PATH`。
|
||||
* 将所有数据加载函数的 `file_path` 参数的默认值从硬编码的字符串改为 `None`。
|
||||
* 在函数内部,如果 `file_path` 为 `None`,则自动使用导入的 `DEFAULT_DATA_PATH`。
|
||||
* 移除了原有的、复杂的、为了猜测正确路径而编写的冗余代码。
|
||||
|
||||
3. **修改 `server/core/predictor.py`**:
|
||||
* 同样从 `server/core/config` 导入 `DEFAULT_DATA_PATH`。
|
||||
* 在初始化 `PharmacyPredictor` 时,如果未提供数据路径,则使用导入的 `DEFAULT_DATA_PATH` 作为默认值。
|
||||
|
||||
### 最终结果
|
||||
通过将数据源路径集中到唯一的配置文件中进行管理,彻底解决了因硬编码路径导致的可移植性问题。项目现在可以在任何环境下可靠地运行。
|
||||
|
||||
---
|
||||
### 未来如何修改数据源(例如,连接到服务器数据库)
|
||||
|
||||
本次重构为将来更换数据源打下了坚实的基础。操作非常简单:
|
||||
|
||||
1. **定位配置文件**: 打开 `server/core/config.py` 文件。
|
||||
|
||||
2. **修改数据源定义**:
|
||||
* **当前 (文件)**:
|
||||
```python
|
||||
DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
|
||||
```
|
||||
* **未来 (数据库示例)**:
|
||||
您可以将这行替换为数据库连接字符串,或者添加新的数据库配置变量。例如:
|
||||
```python
|
||||
# 注释掉或删除旧的文件路径配置
|
||||
# DEFAULT_DATA_PATH = ...
|
||||
|
||||
# 新增数据库连接配置
|
||||
DATABASE_URL = "postgresql://user:password@your_server_ip:5432/your_database_name"
|
||||
```
|
||||
|
||||
3. **修改数据加载逻辑**:
|
||||
* **定位数据加载函数**: 打开 `server/utils/multi_store_data_utils.py`。
|
||||
* **修改 `load_multi_store_data` 函数**:
|
||||
* 引入数据库连接库(如 `sqlalchemy` 或 `psycopg2`)。
|
||||
* 修改函数逻辑,使其使用 `config.py` 中的 `DATABASE_URL` 来连接数据库,并执行SQL查询来获取数据,而不是读取文件。
|
||||
* **示例**:
|
||||
```python
|
||||
from sqlalchemy import create_engine
|
||||
from core.config import DATABASE_URL # 导入新的数据库配置
|
||||
|
||||
def load_multi_store_data(...):
|
||||
# ...
|
||||
engine = create_engine(DATABASE_URL)
|
||||
query = "SELECT * FROM sales_data" # 根据需要构建查询
|
||||
df = pd.read_sql(query, engine)
|
||||
# ... 后续处理逻辑保持不变 ...
|
||||
```
|
||||
|
||||
通过以上步骤,您就可以在不改动项目其他任何部分的情况下,轻松地将数据源从本地文件切换到服务器数据库。
|
101
新需求开发流程.md
Normal file
101
新需求开发流程.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 新需求开发流程
|
||||
|
||||
本文档旨在提供一个标准、安全的新功能开发工作流,涵盖从创建功能分支到最终合并回主开发分支的完整步骤。
|
||||
|
||||
## 核心流程
|
||||
|
||||
采用功能分支(Feature Branch)的工作模式,主要步骤如下:
|
||||
|
||||
1. **同步与切换**:将远程创建的新功能分支(如 `lyf-dev-req0001`)同步到本地并切换。
|
||||
2. **开发与提交**:在本地功能分支上进行开发,并频繁提交改动。
|
||||
3. **推送与备份**:将本地的改动推送到远程功能分支。
|
||||
4. **合并回主线**:当功能开发测试完成后,将其合并回主开发分支(如 `lyf-dev`)。
|
||||
5. **清理分支**:合并完成后,清理已完成使命的功能分支。
|
||||
|
||||
---
|
||||
|
||||
## 详细命令使用步骤
|
||||
|
||||
### 步骤一:同步远程分支到本地
|
||||
|
||||
假设您的同事已经在远程仓库基于 `lyf-dev` 创建了 `lyf-dev-req0001` 分支。
|
||||
|
||||
1. **获取远程所有最新信息**:
|
||||
这个命令会把远程仓库的新分支信息下载到你的本地,但不会做任何修改。
|
||||
```bash
|
||||
git fetch origin
|
||||
```
|
||||
|
||||
2. **创建并切换到本地功能分支**:
|
||||
这个命令会在本地创建一个名为 `lyf-dev-req0001` 的新分支,并自动设置它跟踪远程的 `origin/lyf-dev-req0001` 分支。
|
||||
```bash
|
||||
git checkout lyf-dev-req0001
|
||||
```
|
||||
现在,您已经处于一个干净、独立的功能分支上,可以开始开发了。
|
||||
|
||||
### 步骤二:在功能分支上开发
|
||||
|
||||
在这个分支上进行的所有修改都与 `lyf-dev` 无关,可以放心操作。
|
||||
|
||||
1. **修改代码**:根据需求添加、修改文件。
|
||||
|
||||
2. **提交改动**:
|
||||
建议小步快跑,完成一个小的功能点就提交一次。
|
||||
```bash
|
||||
# 将所有修改添加到暂存区
|
||||
git add .
|
||||
|
||||
# 提交并撰写清晰的说明
|
||||
git commit -m "feat: 完成用户登录接口"
|
||||
```
|
||||
|
||||
3. **推送到远程功能分支**:
|
||||
为了备份代码或与他人协作,需要将本地的提交推送到远程。
|
||||
```bash
|
||||
git push origin lyf-dev-req0001
|
||||
```
|
||||
|
||||
### 步骤三:合并功能到主开发分支 (`lyf-dev`)
|
||||
|
||||
当新功能开发完成,并且经过充分测试后,执行以下步骤将其合并回 `lyf-dev`。
|
||||
|
||||
1. **切换回主开发分支**:
|
||||
```bash
|
||||
git checkout lyf-dev
|
||||
```
|
||||
|
||||
2. **确保 `lyf-dev` 是最新的**:
|
||||
在合并前,务必先从远程拉取 `lyf-dev` 的最新代码,以防他人在此期间有更新。
|
||||
```bash
|
||||
git pull origin lyf-dev
|
||||
```
|
||||
|
||||
3. **合并功能分支**:
|
||||
这是最关键的一步,将 `lyf-dev-req0001` 的所有新功能合并进来。
|
||||
```bash
|
||||
git merge lyf-dev-req0001
|
||||
```
|
||||
- **无冲突**:Git 会自动完成合并。
|
||||
- **有冲突 (Conflict)**:Git 会提示你哪些文件冲突了。你需要手动解决这些文件中的冲突,然后执行 `git add .` 和 `git commit` 来完成合并。
|
||||
|
||||
4. **推送合并后的 `lyf-dev`**:
|
||||
将本地合并好的 `lyf-dev` 分支推送到远程仓库。
|
||||
```bash
|
||||
git push origin lyf-dev
|
||||
```
|
||||
|
||||
### 步骤四:清理分支(可选)
|
||||
|
||||
合并完成后,功能分支的历史使命就完成了。为了保持仓库的整洁,可以删除它。
|
||||
|
||||
1. **删除远程分支**:
|
||||
```bash
|
||||
git push origin --delete lyf-dev-req0001
|
||||
```
|
||||
|
||||
2. **删除本地分支**:
|
||||
```bash
|
||||
git branch -d lyf-dev-req0001
|
||||
```
|
||||
|
||||
遵循以上流程,可以确保您的开发工作流清晰、安全且高效。
|
Loading…
x
Reference in New Issue
Block a user