即时销售报表,毛利润报表

This commit is contained in:
zjf committed 2025-09-23 20:38:00 +08:00
1 parent 6fdc05c396
commit c17731dd86
13 files changed
+2665 -329

No files matched your search

@@ -0,0 +1,90 @@
import request from "/@/utils/request"
/**
* 获取毛利润报表
*/
export function getProfitMarginReport(params?: object) {
return request({
url: "/supermarket/profitMarginReport/report",
method: "get",
params,
})
}
/**
* 获取商品毛利润排名
*/
export function getProductProfitRanking(params?: object) {
return request({
url: "/supermarket/profitMarginReport/productProfitRanking",
method: "get",
params,
})
}
/**
* 获取分类毛利润分析
*/
export function getCategoryProfitAnalysis(params?: object) {
return request({
url: "/supermarket/profitMarginReport/categoryProfitAnalysis",
method: "get",
params,
})
}
/**
* 获取时段毛利润趋势
*/
export function getTimePeriodProfitTrend(params?: object) {
return request({
url: "/supermarket/profitMarginReport/timePeriodProfitTrend",
method: "get",
params,
})
}
/**
* 获取高毛利润商品
*/
export function getHighProfitProducts(params?: object) {
return request({
url: "/supermarket/profitMarginReport/highProfitProducts",
method: "get",
params,
})
}
/**
* 获取低毛利润/亏损商品
*/
export function getLowProfitProducts(params?: object) {
return request({
url: "/supermarket/profitMarginReport/lowProfitProducts",
method: "get",
params,
})
}
/**
* 获取毛利润汇总信息
*/
export function getProfitSummary(params?: object) {
return request({
url: "/supermarket/profitMarginReport/profitSummary",
method: "get",
params,
})
}
/**
* 导出毛利润报表
*/
export function exportProfitMarginReport(params?: object) {
return request({
url: "/supermarket/profitMarginReport/export",
method: "post",
params,
responseType: 'blob'
})
}
@@ -0,0 +1,575 @@
<template>
<el-dialog
:model-value="visible"
:title="dialogTitle"
width="80%"
:before-close="handleClose"
class="product-ranking-dialog"
@update:model-value="handleClose"
>
<!-- 搜索和筛选区域 -->
<div class="search-section">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="商品名称">
<el-input
v-model="searchForm.productName"
placeholder="请输入商品名称"
clearable
style="width: 200px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="商品分类">
<el-select
v-model="searchForm.categoryId"
placeholder="请选择分类"
clearable
style="width: 150px"
>
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
</el-form-item>
<el-form-item label="库存状态">
<el-select
v-model="searchForm.stockStatus"
placeholder="请选择状态"
clearable
style="width: 120px"
>
<el-option label="充足" value="SUFFICIENT" />
<el-option label="不足" value="LOW" />
<el-option label="缺货" value="OUT" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据表格 -->
<el-table
:data="tableData"
stripe
v-loading="loading"
@row-click="handleRowClick"
class="ranking-table"
height="400"
>
<el-table-column prop="ranking" label="排名" width="80" align="center" fixed="left">
<template #default="{ row }">
<el-tag
:type="getRankingTagType(row.ranking)"
size="small"
class="ranking-tag"
>
{{ row.ranking }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="productName" label="商品名称" min-width="200" show-overflow-tooltip fixed="left">
<template #default="{ row }">
<div class="product-info">
<div class="product-name">{{ row.productName }}</div>
<div class="product-code">{{ row.productCode }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="categoryName" label="分类" width="120" show-overflow-tooltip />
<el-table-column prop="salesAmount" label="销售额" width="120" align="right" sortable>
<template #default="{ row }">
{{ formatCurrency(getNumericValue(row.salesAmount)) }}
</template>
</el-table-column>
<el-table-column prop="salesQuantity" label="销量" width="100" align="right" sortable>
<template #default="{ row }">
{{ formatNumber(getNumericValue(row.salesQuantity)) }}
</template>
</el-table-column>
<el-table-column prop="salesCount" label="销售次数" width="100" align="right" sortable>
<template #default="{ row }">
{{ formatNumber(getNumericValue(row.salesCount)) }}
</template>
</el-table-column>
<el-table-column prop="averagePrice" label="平均单价" width="100" align="right" sortable>
<template #default="{ row }">
{{ formatCurrency(getNumericValue(row.averagePrice)) }}
</template>
</el-table-column>
<el-table-column prop="stockQuantity" label="库存" width="100" align="right" sortable>
<template #default="{ row }">
{{ formatNumber(getNumericValue(row.stockQuantity)) }}
</template>
</el-table-column>
<el-table-column prop="stockStatus" label="库存状态" width="100" align="center">
<template #default="{ row }">
<el-tag
:type="getStockStatusType(row.stockStatus)"
size="small"
>
{{ getStockStatusText(row.stockStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="growthRate" label="增长率" width="100" align="right" sortable>
<template #default="{ row }">
<span :class="getGrowthRateClass(getNumericValue(row.growthRate))">
{{ getNumericValue(row.growthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.growthRate) }}%
</span>
</template>
</el-table-column>
<el-table-column prop="yearOverYearGrowthRate" label="同比" width="100" align="right" sortable>
<template #default="{ row }">
<span :class="getGrowthRateClass(getNumericValue(row.yearOverYearGrowthRate))">
{{ getNumericValue(row.yearOverYearGrowthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.yearOverYearGrowthRate) }}%
</span>
</template>
</el-table-column>
<el-table-column prop="monthOverMonthGrowthRate" label="环比" width="100" align="right" sortable>
<template #default="{ row }">
<span :class="getGrowthRateClass(getNumericValue(row.monthOverMonthGrowthRate))">
{{ getNumericValue(row.monthOverMonthGrowthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.monthOverMonthGrowthRate) }}%
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleViewDetail(row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" @click="handleExport">
<el-icon><Download /></el-icon>
导出数据
</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup name="ProductRankingDialog">
import { ref, reactive, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Search, RefreshLeft, Download } from '@element-plus/icons-vue';
import { getTopSellingProducts, getSlowSellingProducts } from '/@/api/supermarketerp/realtimeSalesReport';
import { exportToCSV, formatProductDataForExport } from '../utils/exportUtils';
interface ProductRankingItem {
productId: number;
productName: string;
productCode: string;
categoryId?: string;
categoryName?: string;
salesAmount: number;
salesQuantity: number;
salesCount: number;
averagePrice: number;
ranking: number;
stockQuantity: number;
stockStatus: string;
growthRate: number;
yearOverYearGrowthRate?: number;
monthOverMonthGrowthRate?: number;
}
interface SearchForm {
productName: string;
categoryId: string;
stockStatus: string;
}
interface Pagination {
current: number;
size: number;
total: number;
}
interface Props {
visible: boolean;
type: 'top' | 'slow';
queryParams?: any;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
type: 'top',
queryParams: () => ({})
});
const emit = defineEmits<{
close: [];
viewDetail: [item: ProductRankingItem];
}>();
// 响应式数据
const loading = ref(false);
const tableData = ref<ProductRankingItem[]>([]);
// 搜索表单
const searchForm = reactive<SearchForm>({
productName: '',
categoryId: '',
stockStatus: ''
});
// 分页
const pagination = reactive<Pagination>({
current: 1,
size: 20,
total: 0
});
// 分类数据
const categories = ref([
{ id: '1', name: '水果' },
{ id: '2', name: '蔬菜' },
{ id: '3', name: '肉类' },
{ id: '4', name: '乳制品' },
{ id: '5', name: '饮料' },
{ id: '6', name: '零食' },
{ id: '7', name: '日用品' }
]);
// 计算属性
const dialogTitle = computed(() => {
return props.type === 'top' ? '畅销品排行榜' : '滞销品排行榜';
});
// 监听弹窗显示状态
watch(() => props.visible, (newVal) => {
if (newVal) {
loadData();
}
});
// 加载数据
const loadData = async () => {
loading.value = true;
try {
const params = {
...props.queryParams,
...searchForm,
current: pagination.current,
size: pagination.size
};
let response;
if (props.type === 'top') {
response = await getTopSellingProducts(params);
} else {
response = await getSlowSellingProducts(params);
}
if (response.code === 0 && response.data) {
tableData.value = response.data.records || response.data || [];
pagination.total = response.data.total || 0;
} else {
throw new Error(response.msg || '数据加载失败');
}
} catch (error) {
console.error('加载数据失败:', error);
ElMessage.error('数据加载失败');
// 使用模拟数据
tableData.value = getMockData();
pagination.total = tableData.value.length;
} finally {
loading.value = false;
}
};
// 模拟数据
const getMockData = (): ProductRankingItem[] => {
const data: ProductRankingItem[] = [];
const products = [
{ name: '苹果', code: 'P001', category: '水果' },
{ name: '牛奶', code: 'M002', category: '乳制品' },
{ name: '面包', code: 'B003', category: '烘焙' },
{ name: '矿泉水', code: 'W004', category: '饮料' },
{ name: '鸡蛋', code: 'E005', category: '蛋类' },
{ name: '香蕉', code: 'B006', category: '水果' },
{ name: '酸奶', code: 'Y007', category: '乳制品' },
{ name: '饼干', code: 'C008', category: '零食' },
{ name: '可乐', code: 'K009', category: '饮料' },
{ name: '橙子', code: 'O010', category: '水果' }
];
for (let i = 0; i < 50; i++) {
const product = products[i % products.length];
const ranking = i + 1;
data.push({
productId: 1000 + i,
productName: `${product.name}${i > 9 ? ` (${i + 1})` : ''}`,
productCode: product.code.replace(/\d+/, String(i + 1).padStart(3, '0')),
categoryId: String((i % 7) + 1),
categoryName: product.category,
salesAmount: Math.floor(Math.random() * 10000) + 1000,
salesQuantity: Math.floor(Math.random() * 100) + 10,
salesCount: Math.floor(Math.random() * 50) + 5,
averagePrice: Math.floor(Math.random() * 100) + 10,
ranking: ranking,
stockQuantity: Math.floor(Math.random() * 200) + 10,
stockStatus: ['SUFFICIENT', 'LOW', 'OUT'][Math.floor(Math.random() * 3)],
growthRate: Math.floor(Math.random() * 40) - 20,
yearOverYearGrowthRate: Math.floor(Math.random() * 30) - 15,
monthOverMonthGrowthRate: Math.floor(Math.random() * 20) - 10
});
}
return data;
};
// 搜索
const handleSearch = () => {
pagination.current = 1;
loadData();
};
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
productName: '',
categoryId: '',
stockStatus: ''
});
pagination.current = 1;
loadData();
};
// 分页大小变化
const handleSizeChange = (size: number) => {
pagination.size = size;
pagination.current = 1;
loadData();
};
// 当前页变化
const handleCurrentChange = (page: number) => {
pagination.current = page;
loadData();
};
// 行点击
const handleRowClick = (item: ProductRankingItem) => {
handleViewDetail(item);
};
// 查看详情
const handleViewDetail = (item: ProductRankingItem) => {
emit('viewDetail', item);
};
// 关闭弹窗
const handleClose = () => {
emit('close');
};
// 导出数据
const handleExport = () => {
try {
if (tableData.value.length === 0) {
ElMessage.warning('没有数据可导出');
return;
}
const exportData = formatProductDataForExport(tableData.value);
const filename = `${dialogTitle.value}_${new Date().toISOString().slice(0, 10)}`;
exportToCSV(exportData, filename);
ElMessage.success('数据导出成功');
} catch (error) {
console.error('导出失败:', error);
ElMessage.error('数据导出失败');
}
};
// 获取排名标签类型
const getRankingTagType = (ranking: number) => {
if (ranking <= 3) return 'danger';
if (ranking <= 10) return 'warning';
return 'primary';
};
// 获取增长率样式类
const getGrowthRateClass = (rate: number) => {
if (rate > 0) return 'growth-up';
if (rate < 0) return 'growth-down';
return 'growth-stable';
};
// 获取库存状态标签类型
const getStockStatusType = (status: string) => {
switch (status) {
case 'SUFFICIENT':
return 'success';
case 'LOW':
return 'warning';
case 'OUT':
return 'danger';
default:
return 'info';
}
};
// 获取库存状态文本
const getStockStatusText = (status: string) => {
switch (status) {
case 'SUFFICIENT':
return '充足';
case 'LOW':
return '不足';
case 'OUT':
return '缺货';
default:
return '未知';
}
};
// 格式化货币
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(value);
};
// 格式化数字
const formatNumber = (value: number) => {
return new Intl.NumberFormat('zh-CN').format(value);
};
// 获取数值(处理复杂数据格式)
const getNumericValue = (value: any) => {
if (typeof value === 'number') {
return value;
}
if (typeof value === 'object' && value !== null) {
if (value.parsedValue !== undefined) {
return value.parsedValue;
}
if (value.source !== undefined) {
return parseFloat(value.source) || 0;
}
}
if (typeof value === 'string') {
return parseFloat(value) || 0;
}
return 0;
};
</script>
<style lang="scss" scoped>
.product-ranking-dialog {
.search-section {
margin-bottom: 20px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 6px;
.search-form {
.el-form-item {
margin-bottom: 12px;
}
}
}
.ranking-table {
.ranking-tag {
font-weight: 600;
}
.product-info {
.product-name {
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.product-code {
font-size: 12px;
color: #909399;
}
}
.growth-up {
color: #67c23a;
font-weight: 500;
}
.growth-down {
color: #f56c6c;
font-weight: 500;
}
.growth-stable {
color: #909399;
}
}
.pagination-section {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
</style>
@@ -337,3 +337,4 @@ const handleCurrentChange = (page: number) => {
}
}
</style>
@@ -189,3 +189,4 @@ defineExpose({
width: 100%;
}
</style>
@@ -201,3 +201,4 @@ const trendClass = computed(() => {
}
}
</style>
@@ -5,3 +5,4 @@
export { default as SalesChart } from './SalesChart.vue';
export { default as SummaryCard } from './SummaryCard.vue';
export { default as ProductRankingTable } from './ProductRankingTable.vue';
@@ -205,3 +205,4 @@ export const SUCCESS_MESSAGES = {
DATA_EXPORTED: '数据导出成功',
CACHE_REFRESHED: '缓存刷新成功',
};
+160 -328
View File
@@ -254,7 +254,7 @@
<template #header>
<div class="card-header">
<span>畅销品TOP10</span>
<el-button size="small" @click="viewAllTopProducts">查看全部</el-button>
<!-- <el-button size="small" @click="viewAllTopProducts">查看全部</el-button> -->
</div>
</template>
<el-table :data="topSellingProducts" stripe v-loading="loading">
@@ -299,7 +299,7 @@
<template #header>
<div class="card-header">
<span>滞销品TOP10</span>
<el-button size="small" @click="viewAllSlowProducts">查看全部</el-button>
<!-- <el-button size="small" @click="viewAllSlowProducts">查看全部</el-button> -->
</div>
</template>
<el-table :data="slowSellingProducts" stripe v-loading="loading">
@@ -415,11 +415,20 @@
</el-table-column>
</el-table>
</el-card>
<!-- 商品排名详情弹窗 -->
<!-- <ProductRankingDialog
:visible="dialogVisible"
:type="dialogType"
:query-params="queryForm"
@close="handleDialogClose"
@view-detail="handleViewProductDetail"
/> -->
</div>
</template>
<script lang="ts" setup name="RealtimeSalesReport">
import { ref, reactive, onMounted, nextTick } from 'vue';
import { ref, reactive, onMounted, nextTick, watch } from 'vue';
import { ElMessage } from 'element-plus';
import {
Refresh,
@@ -442,15 +451,21 @@ import {
getCustomerAnalysis,
refreshRealtimeData,
} from '/@/api/supermarketerp/realtimeSalesReport';
// import ProductRankingDialog from './components/ProductRankingDialog.vue';
// 响应式数据
const refreshing = ref(false);
const loading = ref(false);
const isInitialized = ref(false);
const debounceTimer = ref<NodeJS.Timeout | null>(null);
const debounceTimer = ref<number | null>(null);
const salesTrendChart = ref<HTMLElement>();
const customerAnalysisChart = ref<HTMLElement>();
// 弹窗相关数据(已注释)
// const dialogVisible = ref(false);
// const dialogType = ref<'top' | 'slow'>('top');
// const selectedProduct = ref(null);
// 查询表单
const queryForm = reactive({
queryType: 'day',
@@ -464,16 +479,16 @@ const queryForm = reactive({
});
// 高级选项显示状态
const advancedOptionsVisible = ref([]);
const advancedOptionsVisible = ref<string[]>([]);
// 数据
const salesSummary = ref();
const topSellingProducts = ref([]);
const slowSellingProducts = ref([]);
const timePeriodSales = ref([]);
const customerAnalysis = ref();
const salesTrend = ref([]);
const expandedRows = ref([]);
const salesSummary = ref<any>();
const topSellingProducts = ref<any[]>([]);
const slowSellingProducts = ref<any[]>([]);
const timePeriodSales = ref<any[]>([]);
const customerAnalysis = ref<any>();
const salesTrend = ref<any[]>([]);
const expandedRows = ref<string[]>([]);
// 图表实例
let salesTrendChartInstance: echarts.ECharts | null = null;
@@ -492,9 +507,11 @@ const initQueryForm = () => {
queryForm.startDate = today.toISOString().slice(0, 10);
queryForm.endDate = now.toISOString().slice(0, 10);
} else if (queryForm.queryType === 'month') {
// 确保按月查询时设置正确的默认值
const thisMonth = now.toISOString().slice(0, 7);
queryForm.startDate = thisMonth;
queryForm.endDate = thisMonth;
console.log('初始化按月查询默认值:', { startDate: queryForm.startDate, endDate: queryForm.endDate });
}
};
@@ -596,20 +613,99 @@ watch(
{ deep: true }
);
// 处理查询参数,确保日期格式正确
const processQueryParams = (params: any) => {
const processedParams = { ...params };
// 如果是按月查询,需要将YYYY-MM格式转换为完整的日期范围
if (params.queryType === 'month') {
if (params.startDate) {
try {
// 确保日期格式正确,处理可能的格式问题
let dateStr = params.startDate;
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
// 已经是 YYYY-MM 格式
const startDate = new Date(dateStr + '-01');
if (isNaN(startDate.getTime())) {
throw new Error('Invalid start date');
}
processedParams.startDate = startDate.toISOString().slice(0, 10);
console.log(`按月查询 - 开始日期转换: ${params.startDate} -> ${processedParams.startDate}`);
} else {
console.warn('按月查询 - 开始日期格式不正确:', params.startDate);
// 如果格式不正确,尝试使用当前月份
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
console.log(`按月查询 - 使用默认开始日期: ${processedParams.startDate}`);
}
} catch (error) {
console.error('按月查询 - 开始日期转换失败:', error);
// 使用当前月份作为默认值
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
}
}
if (params.endDate) {
try {
// 确保日期格式正确,处理可能的格式问题
let dateStr = params.endDate;
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
// 已经是 YYYY-MM 格式
const endDate = new Date(dateStr + '-01');
if (isNaN(endDate.getTime())) {
throw new Error('Invalid end date');
}
endDate.setMonth(endDate.getMonth() + 1, 0); // 设置为下个月的第0天,即当前月的最后一天
processedParams.endDate = endDate.toISOString().slice(0, 10);
console.log(`按月查询 - 结束日期转换: ${params.endDate} -> ${processedParams.endDate}`);
} else {
console.warn('按月查询 - 结束日期格式不正确:', params.endDate);
// 如果格式不正确,尝试使用当前月份
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
console.log(`按月查询 - 使用默认结束日期: ${processedParams.endDate}`);
}
} catch (error) {
console.error('按月查询 - 结束日期转换失败:', error);
// 使用当前月份作为默认值
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
}
}
}
console.log('处理后的查询参数:', processedParams);
return processedParams;
};
// 加载数据
const loadData = async () => {
loading.value = true;
try {
// 处理查询参数
const processedParams = processQueryParams(queryForm);
// 加载完整报表数据
const reportData = await getRealtimeSalesReport(queryForm);
const reportData = await getRealtimeSalesReport(processedParams);
if (reportData.code === 0 && reportData.data) {
// 使用后端返回的数据
salesSummary.value = reportData.data.salesSummary || getMockSalesSummary();
salesSummary.value = reportData.data.salesSummary || {};
topSellingProducts.value = reportData.data.topSellingProducts || [];
slowSellingProducts.value = reportData.data.slowSellingProducts || [];
timePeriodSales.value = reportData.data.timePeriodSales || [];
customerAnalysis.value = reportData.data.customerAnalysis || getMockCustomerAnalysis();
customerAnalysis.value = reportData.data.customerAnalysis || {};
salesTrend.value = reportData.data.salesTrend || [];
ElMessage.success('数据加载成功');
@@ -622,245 +718,31 @@ const loadData = async () => {
renderCharts();
} catch (error) {
console.error('加载数据失败:', error);
// 使用模拟数据
salesSummary.value = getMockSalesSummary();
topSellingProducts.value = getMockTopProducts();
slowSellingProducts.value = getMockSlowProducts();
timePeriodSales.value = getMockTimePeriodSales();
customerAnalysis.value = getMockCustomerAnalysis();
salesTrend.value = []; // 空数组,不显示模拟数据
// 清空数据
salesSummary.value = {};
topSellingProducts.value = [];
slowSellingProducts.value = [];
timePeriodSales.value = [];
customerAnalysis.value = {};
salesTrend.value = [];
await nextTick();
renderCharts();
// 根据错误类型显示不同的提示信息
if (error.message && error.message.includes('权限')) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('权限')) {
ElMessage.error('权限不足,无法访问即时销售报表');
} else if (error.message && error.message.includes('网络')) {
} else if (errorMessage.includes('网络')) {
ElMessage.error('网络连接异常,请检查网络设置');
} else {
ElMessage.warning('数据加载失败,使用模拟数据展示');
ElMessage.error('数据加载失败,请稍后重试');
}
} finally {
loading.value = false;
}
};
// 模拟数据生成函数 - 根据后端API文档调整
const getMockSalesSummary = () => ({
totalSalesAmount: 12500.50,
totalSalesCount: 45,
averageCustomerValue: 277.79,
productVarietyCount: 23,
totalSalesQuantity: 156.5,
salesGrowthRate: 15.5,
salesGrowthRateMonth: 8.2,
realtimeStatus: 'MEDIUM',
});
const getMockTopProducts = () => [
{
productId: 1001,
productName: '苹果',
productCode: 'P001',
salesAmount: 1250.00,
salesQuantity: 50.0,
salesCount: 12,
averagePrice: 25.00,
ranking: 1,
stockQuantity: 200.0,
stockStatus: 'SUFFICIENT',
growthRate: 12.5
},
{
productId: 1002,
productName: '牛奶',
productCode: 'M002',
salesAmount: 980.00,
salesQuantity: 49.0,
salesCount: 10,
averagePrice: 20.00,
ranking: 2,
stockQuantity: 150.0,
stockStatus: 'SUFFICIENT',
growthRate: 8.3
},
{
productId: 1003,
productName: '面包',
productCode: 'B003',
salesAmount: 850.00,
salesQuantity: 34.0,
salesCount: 8,
averagePrice: 25.00,
ranking: 3,
stockQuantity: 5.0,
stockStatus: 'LOW',
growthRate: 15.2
},
{
productId: 1004,
productName: '矿泉水',
productCode: 'W004',
salesAmount: 720.00,
salesQuantity: 36.0,
salesCount: 7,
averagePrice: 20.00,
ranking: 4,
stockQuantity: 0.0,
stockStatus: 'OUT',
growthRate: 6.7
},
{
productId: 1005,
productName: '鸡蛋',
productCode: 'E005',
salesAmount: 680.00,
salesQuantity: 68.0,
salesCount: 6,
averagePrice: 10.00,
ranking: 5,
stockQuantity: 100.0,
stockStatus: 'SUFFICIENT',
growthRate: 4.2
},
];
const getMockSlowProducts = () => [
{
productId: 2001,
productName: '高级红酒',
productCode: 'W001',
salesAmount: 120.00,
salesQuantity: 1.2,
salesCount: 1,
averagePrice: 100.00,
ranking: 1,
stockQuantity: 50.0,
stockStatus: 'SUFFICIENT',
growthRate: -15.3
},
{
productId: 2002,
productName: '进口巧克力',
productCode: 'C002',
salesAmount: 80.00,
salesQuantity: 2.0,
salesCount: 2,
averagePrice: 40.00,
ranking: 2,
stockQuantity: 8.0,
stockStatus: 'LOW',
growthRate: -8.7
},
{
productId: 2003,
productName: '有机蔬菜',
productCode: 'V003',
salesAmount: 60.00,
salesQuantity: 3.0,
salesCount: 1,
averagePrice: 20.00,
ranking: 3,
stockQuantity: 15.0,
stockStatus: 'SUFFICIENT',
growthRate: -12.1
},
{
productId: 2004,
productName: '高端茶叶',
productCode: 'T004',
salesAmount: 40.00,
salesQuantity: 0.8,
salesCount: 1,
averagePrice: 50.00,
ranking: 4,
stockQuantity: 0.0,
stockStatus: 'OUT',
growthRate: -20.5
},
{
productId: 2005,
productName: '进口咖啡',
productCode: 'C005',
salesAmount: 30.00,
salesQuantity: 0.6,
salesCount: 1,
averagePrice: 50.00,
ranking: 5,
stockQuantity: 2.0,
stockStatus: 'LOW',
growthRate: -25.8
},
];
const getMockTimePeriodSales = () => {
const data = [];
const products = [
{ name: '苹果', code: 'P001', category: '水果', unit: 'kg', unitName: 'kg' },
{ name: '牛奶', code: 'M002', category: '乳制品', unit: '瓶', unitName: '瓶' },
{ name: '面包', code: 'B003', category: '烘焙', unit: '个', unitName: '个' },
{ name: '矿泉水', code: 'W004', category: '饮料', unit: '瓶', unitName: '瓶' },
{ name: '鸡蛋', code: 'E005', category: '蛋类', unit: '个', unitName: '个' },
];
for (let i = 0; i < 24; i++) {
const productCount = Math.floor(Math.random() * 3) + 1;
const productDetails = [];
let totalAmount = 0;
let totalQuantity = 0;
for (let j = 0; j < productCount; j++) {
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 5) + 1;
const unitPrice = Math.floor(Math.random() * 50) + 10;
const amount = quantity * unitPrice;
productDetails.push({
productId: `mock_${i}_${j}`,
productName: product.name,
productCode: product.code,
categoryId: `cat_${j}`,
categoryName: product.category,
unit: product.unit,
unitName: product.unitName,
quantity: quantity,
unitPrice: unitPrice,
totalAmount: amount,
salesCount: 1,
});
totalAmount += amount;
totalQuantity += quantity;
}
data.push({
timeLabel: `${i.toString().padStart(2, '0')}:00`,
salesAmount: totalAmount,
salesCount: productCount,
customerValue: totalAmount / productCount,
salesQuantity: totalQuantity,
growthRate: Math.floor(Math.random() * 20) - 10,
productDetails: productDetails,
});
}
return data;
};
const getMockCustomerAnalysis = () => ({
averageCustomerValue: 277.79,
maxCustomerValue: 850.00,
minCustomerValue: 15.50,
medianCustomerValue: 250.00,
highValueOrderRatio: 25.5,
valueRanges: [
{ rangeLabel: '0-50元', orderCount: 8, percentage: 17.78 },
{ rangeLabel: '50-100元', orderCount: 12, percentage: 26.67 },
{ rangeLabel: '100-200元', orderCount: 15, percentage: 33.33 },
{ rangeLabel: '200-500元', orderCount: 8, percentage: 17.78 },
{ rangeLabel: '500+元', orderCount: 2, percentage: 4.44 },
],
});
// 刷新数据
@@ -892,30 +774,48 @@ const refreshCharts = async () => {
// 刷新时段数据
const refreshTimePeriodData = async () => {
try {
const processedParams = processQueryParams(queryForm);
const timePeriodData = await getTimePeriodSales({
...queryForm,
...processedParams,
current: 1,
size: 50,
});
timePeriodSales.value = timePeriodData.data?.records || getMockTimePeriodSales();
timePeriodSales.value = timePeriodData.data?.records || [];
ElMessage.success('时段数据刷新成功');
} catch (error) {
console.error('刷新时段数据失败:', error);
timePeriodSales.value = getMockTimePeriodSales();
ElMessage.warning('使用模拟数据');
timePeriodSales.value = [];
ElMessage.error('时段数据刷新失败');
}
};
// 查看全部畅销品
const viewAllTopProducts = () => {
ElMessage.info('查看全部畅销品功能开发中...');
};
// 查看全部畅销品(已注释)
// const viewAllTopProducts = () => {
// dialogType.value = 'top';
// dialogVisible.value = true;
// };
// 查看全部滞销品(已注释)
// const viewAllSlowProducts = () => {
// dialogType.value = 'slow';
// dialogVisible.value = true;
// };
// 关闭弹窗(已注释)
// const handleDialogClose = () => {
// dialogVisible.value = false;
// selectedProduct.value = null;
// };
// 查看商品详情(已注释)
// const handleViewProductDetail = (product: any) => {
// selectedProduct.value = product;
// ElMessage.info(`查看商品详情: ${product.productName}`);
// // 这里可以打开商品详情弹窗或跳转到详情页面
// console.log('商品详情:', product);
// };
// 查看全部滞销品
const viewAllSlowProducts = () => {
ElMessage.info('查看全部滞销品功能开发中...');
};
// 渲染销售趋势图表
const renderSalesTrendChart = () => {
@@ -926,9 +826,9 @@ const renderSalesTrendChart = () => {
}
// 使用后端返回的销售趋势数据
let xAxisData = [];
let salesData = [];
let countData = [];
let xAxisData: string[] = [];
let salesData: number[] = [];
let countData: number[] = [];
if (salesTrend.value && salesTrend.value.length > 0) {
// 使用后端返回的真实数据
@@ -1020,28 +920,22 @@ const renderCustomerAnalysisChart = () => {
}
// 使用后端数据结构
let data = [];
let data: any[] = [];
if (customerAnalysis.value && customerAnalysis.value.valueRanges) {
data = customerAnalysis.value.valueRanges.map(range => ({
data = customerAnalysis.value.valueRanges.map((range: any) => ({
name: range.rangeLabel,
value: range.orderCount,
percentage: range.percentage
}));
} else {
// 模拟数据
data = [
{ name: '0-50元', value: 25, percentage: 15.5 },
{ name: '50-100元', value: 35, percentage: 21.7 },
{ name: '100-200元', value: 20, percentage: 12.4 },
{ name: '200-500元', value: 15, percentage: 9.3 },
{ name: '500+元', value: 5, percentage: 3.1 },
];
// 没有数据时显示空数组
data = [];
}
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
formatter: function(params: any) {
return `${params.name}<br/>订单数: ${params.value}<br/>占比: ${params.data.percentage}%`;
},
},
@@ -1171,68 +1065,6 @@ const getNumericValue = (value: any) => {
};
// 根据类型生成模拟时段数据
const getMockTimePeriodSalesByType = (type: string) => {
const data = [];
if (type === 'hour') {
// 按小时生成24小时数据
for (let i = 0; i < 24; i++) {
data.push({
timePeriod: `${i}:00`,
timePeriodName: `${i}:00-${i + 1}:00`,
salesAmount: Math.floor(Math.random() * 5000) + 1000,
salesCount: Math.floor(Math.random() * 50) + 10,
salesQuantity: Math.floor(Math.random() * 100) + 20,
averageCustomerPrice: Math.floor(Math.random() * 50) + 50,
customerCount: Math.floor(Math.random() * 20) + 5,
productVarietyCount: Math.floor(Math.random() * 30) + 10,
yearOverYearSalesAmount: Math.floor(Math.random() * 1000) - 500,
monthOverMonthSalesAmount: Math.floor(Math.random() * 500) - 250,
});
}
} else if (type === 'day') {
// 按天生成7天数据
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().slice(0, 10);
data.push({
timePeriod: dateStr,
timePeriodName: dateStr,
salesAmount: Math.floor(Math.random() * 20000) + 5000,
salesCount: Math.floor(Math.random() * 200) + 50,
salesQuantity: Math.floor(Math.random() * 500) + 100,
averageCustomerPrice: Math.floor(Math.random() * 100) + 50,
customerCount: Math.floor(Math.random() * 50) + 20,
productVarietyCount: Math.floor(Math.random() * 50) + 30,
yearOverYearSalesAmount: Math.floor(Math.random() * 5000) - 2500,
monthOverMonthSalesAmount: Math.floor(Math.random() * 2000) - 1000,
});
}
} else if (type === 'month') {
// 按月生成12个月数据
for (let i = 11; i >= 0; i--) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const monthStr = date.toISOString().slice(0, 7);
data.push({
timePeriod: monthStr,
timePeriodName: monthStr,
salesAmount: Math.floor(Math.random() * 100000) + 50000,
salesCount: Math.floor(Math.random() * 1000) + 500,
salesQuantity: Math.floor(Math.random() * 2000) + 1000,
averageCustomerPrice: Math.floor(Math.random() * 200) + 100,
customerCount: Math.floor(Math.random() * 200) + 100,
productVarietyCount: Math.floor(Math.random() * 100) + 50,
yearOverYearSalesAmount: Math.floor(Math.random() * 20000) - 10000,
monthOverMonthSalesAmount: Math.floor(Math.random() * 10000) - 5000,
});
}
}
return data;
};
// 组件挂载
onMounted(async () => {
@@ -0,0 +1,137 @@
/**
* 数据导出工具函数
*/
// 导出为CSV格式
export function exportToCSV(data: any[], filename: string, headers?: string[]) {
if (!data || data.length === 0) {
console.warn('没有数据可导出');
return;
}
// 获取表头
const csvHeaders = headers || Object.keys(data[0]);
// 构建CSV内容
let csvContent = '\uFEFF'; // 添加BOM以支持中文
csvContent += csvHeaders.join(',') + '\n';
// 添加数据行
data.forEach(row => {
const values = csvHeaders.map(header => {
let value = row[header];
if (value === null || value === undefined) {
value = '';
}
// 处理包含逗号的值
if (typeof value === 'string' && value.includes(',')) {
value = `"${value}"`;
}
return value;
});
csvContent += values.join(',') + '\n';
});
// 创建下载链接
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `${filename}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// 导出为Excel格式(需要xlsx库)
export function exportToExcel(data: any[], filename: string, sheetName: string = 'Sheet1') {
// 这里需要安装 xlsx 库: npm install xlsx
// import * as XLSX from 'xlsx';
console.log('Excel导出功能需要安装xlsx库');
console.log('安装命令: npm install xlsx');
console.log('数据:', data);
console.log('文件名:', filename);
console.log('工作表名:', sheetName);
// 临时使用CSV导出
exportToCSV(data, filename);
}
// 格式化商品排名数据用于导出
export function formatProductDataForExport(data: any[]) {
return data.map((item, index) => ({
排名: item.ranking || index + 1,
商品名称: item.productName || '',
商品编码: item.productCode || '',
分类: item.categoryName || '',
销售额: item.salesAmount || 0,
销量: item.salesQuantity || 0,
销售次数: item.salesCount || 0,
平均单价: item.averagePrice || 0,
库存: item.stockQuantity || 0,
库存状态: getStockStatusText(item.stockStatus),
增长率: `${item.growthRate || 0}%`,
同比: `${item.yearOverYearGrowthRate || 0}%`,
环比: `${item.monthOverMonthGrowthRate || 0}%`
}));
}
// 获取库存状态文本
function getStockStatusText(status: string) {
switch (status) {
case 'SUFFICIENT':
return '充足';
case 'LOW':
return '不足';
case 'OUT':
return '缺货';
default:
return '未知';
}
}
// 格式化销售汇总数据用于导出
export function formatSalesSummaryForExport(data: any) {
return [{
总销售额: data.totalSalesAmount || 0,
总销售笔数: data.totalSalesCount || 0,
平均客单价: data.averageCustomerValue || 0,
商品种类数: data.productVarietyCount || 0,
总销量: data.totalSalesQuantity || 0,
销售增长率: `${data.salesGrowthRate || 0}%`,
月度增长率: `${data.salesGrowthRateMonth || 0}%`,
实时状态: getRealtimeStatusText(data.realtimeStatus)
}];
}
// 获取实时状态文本
function getRealtimeStatusText(status: string) {
switch (status) {
case 'HIGH':
return '高';
case 'MEDIUM':
return '中';
case 'LOW':
return '低';
default:
return '未知';
}
}
// 格式化时段销售数据用于导出
export function formatTimePeriodDataForExport(data: any[]) {
return data.map(item => ({
时段: item.timeLabel || '',
销售额: item.salesAmount || 0,
销售笔数: item.salesCount || 0,
客单价: item.customerValue || 0,
销量: item.salesQuantity || 0,
增长率: `${item.growthRate || 0}%`
}));
}
@@ -194,3 +194,4 @@ export const TABLE_CONFIG = {
size: 'default' as const,
showOverflowTooltip: true,
};
@@ -0,0 +1,367 @@
# 毛利润报表业务代码整理
## 代码结构优化建议
### 1. 服务层重构
#### 1.1 将大方法拆分为小方法
```java
@Service
public class ProfitMarginReportServiceImpl implements ProfitMarginReportService {
// 主入口方法 - 保持简洁
@Override
public ProfitMarginReportVO getProfitMarginReport(ProfitMarginReportQueryDTO queryDTO) {
ProfitMarginReportVO reportVO = new ProfitMarginReportVO();
reportVO.setReportTime(LocalDateTime.now());
reportVO.setTimeRange(buildTimeRange(queryDTO));
// 并行获取各类数据
CompletableFuture<ProfitSummaryVO> profitSummaryFuture =
CompletableFuture.supplyAsync(() -> getProfitSummary(queryDTO));
CompletableFuture<List<ProductProfitVO>> productRankingFuture =
CompletableFuture.supplyAsync(() -> getProductProfitRankingData(queryDTO));
CompletableFuture<List<CategoryProfitVO>> categoryAnalysisFuture =
CompletableFuture.supplyAsync(() -> getCategoryProfitAnalysisData(queryDTO));
CompletableFuture<List<TimePeriodProfitVO>> timeTrendFuture =
CompletableFuture.supplyAsync(() -> getTimePeriodProfitTrendData(queryDTO));
try {
reportVO.setProfitSummary(profitSummaryFuture.get());
reportVO.setProductProfitRanking(productRankingFuture.get());
reportVO.setCategoryProfitAnalysis(categoryAnalysisFuture.get());
reportVO.setTimePeriodProfitTrend(timeTrendFuture.get());
// 基于商品排名数据生成高/低毛利润商品
List<ProductProfitVO> allProducts = productRankingFuture.get();
reportVO.setHighProfitProducts(filterHighProfitProducts(allProducts));
reportVO.setLowProfitProducts(filterLowProfitProducts(allProducts));
} catch (Exception e) {
log.error("获取毛利润报表数据失败", e);
throw new BusinessException("获取毛利润报表数据失败");
}
return reportVO;
}
}
```
#### 1.2 数据查询优化
```java
@Component
public class ProfitMarginDataQueryService {
/**
* 获取销售明细数据 - 统一入口
*/
public List<SmErpInventoryRecordsItemEntity> getSalesItems(ProfitMarginReportQueryDTO queryDTO) {
// 1. 先筛选商品ID(如果有商品筛选条件)
Set<Long> filteredProductIds = getFilteredProductIds(queryDTO);
// 2. 查询销售明细
return querySalesItems(queryDTO, filteredProductIds);
}
/**
* 批量获取商品信息
*/
public Map<Long, SmErpSpuEntity> getProductMap(Set<Long> productIds) {
if (CollectionUtils.isEmpty(productIds)) {
return new HashMap<>();
}
List<SmErpSpuEntity> products = spuMapper.selectList(
new LambdaQueryWrapper<SmErpSpuEntity>()
.in(SmErpSpuEntity::getId, productIds)
);
return products.stream()
.collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product));
}
/**
* 批量获取分类信息
*/
public Map<Long, SmErpCategoryEntity> getCategoryMap(Set<Long> categoryIds) {
if (CollectionUtils.isEmpty(categoryIds)) {
return new HashMap<>();
}
List<SmErpCategoryEntity> categories = categoryMapper.selectList(
new LambdaQueryWrapper<SmErpCategoryEntity>()
.in(SmErpCategoryEntity::getId, categoryIds)
);
return categories.stream()
.collect(Collectors.toMap(SmErpCategoryEntity::getId, category -> category));
}
}
```
#### 1.3 计算服务分离
```java
@Component
public class ProfitCalculationService {
/**
* 计算商品毛利润数据
*/
public ProductProfitVO calculateProductProfit(
SmErpSpuEntity product,
List<SmErpInventoryRecordsItemEntity> items,
Map<Long, SmErpUnitEntity> unitMap,
Map<Long, SmErpCategoryEntity> categoryMap) {
// 过滤补差价商品
if (isPriceAdjustmentProduct(product.getProductName())) {
return null;
}
// 计算销售数据
BigDecimal salesAmount = calculateSalesAmount(items);
double salesQuantity = calculateSalesQuantity(items);
int salesCount = items.size();
// 计算成本数据
BigDecimal costPrice = getActualCostPrice(product, items.get(0), unitMap);
BigDecimal costAmount = costPrice.multiply(new BigDecimal(salesQuantity));
// 计算毛利润
BigDecimal profitAmount = salesAmount.subtract(costAmount);
BigDecimal marginRate = calculateMarginRate(salesAmount, profitAmount);
// 构建结果
ProductProfitVO profitVO = new ProductProfitVO();
profitVO.setProductId(product.getId());
profitVO.setProductName(product.getProductName());
profitVO.setProductCode(product.getProductCode());
profitVO.setSalesAmount(salesAmount);
profitVO.setCostAmount(costAmount);
profitVO.setProfitAmount(profitAmount);
profitVO.setMarginRate(marginRate);
profitVO.setSalesQuantity(salesQuantity);
profitVO.setSalesCount(salesCount);
profitVO.setProfitStatus(determineProfitStatus(profitAmount));
// 设置分类信息
if (product.getCategoryId() != null) {
SmErpCategoryEntity category = categoryMap.get(product.getCategoryId());
if (category != null) {
profitVO.setCategoryId(category.getId());
profitVO.setCategoryName(category.getName());
}
}
// 设置单位信息
if (!items.isEmpty() && items.get(0).getUnitId() != null) {
SmErpUnitEntity unit = unitMap.get(items.get(0).getUnitId());
if (unit != null) {
profitVO.setUnit(unit.getUnit());
profitVO.setUnitName(unit.getUnit());
}
}
return profitVO;
}
/**
* 计算毛利率
*/
private BigDecimal calculateMarginRate(BigDecimal salesAmount, BigDecimal profitAmount) {
if (salesAmount.compareTo(BigDecimal.ZERO) > 0) {
return profitAmount.divide(salesAmount, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
}
return BigDecimal.ZERO;
}
/**
* 判断利润状态
*/
private String determineProfitStatus(BigDecimal profitAmount) {
if (profitAmount.compareTo(BigDecimal.ZERO) > 0) {
return "profitable";
} else if (profitAmount.compareTo(BigDecimal.ZERO) < 0) {
return "loss";
} else {
return "break_even";
}
}
}
```
### 2. 查询优化
#### 2.1 使用MyBatis-Plus的关联查询
```java
@Mapper
public interface SmErpInventoryRecordsItemMapper extends BaseMapper<SmErpInventoryRecordsItemEntity> {
/**
* 关联查询销售明细和主表信息
*/
@Select("SELECT item.*, record.inventory_date, record.create_time " +
"FROM sm_erp_inventory_records_item item " +
"INNER JOIN sm_erp_inventory_records record ON item.inventory_id = record.id " +
"WHERE record.inventory_type = '5' AND record.order_type = '2' " +
"AND record.inventory_date >= #{startDate} AND record.inventory_date <= #{endDate} " +
"LIMIT #{limit}")
List<SalesItemWithDateVO> selectSalesItemsWithDate(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("limit") int limit);
}
```
#### 2.2 缓存优化
```java
@Service
public class ProfitMarginCacheService {
@Cacheable(value = "productInfo", key = "#productIds")
public Map<Long, SmErpSpuEntity> getProductMap(Set<Long> productIds) {
// 查询逻辑
}
@Cacheable(value = "categoryInfo", key = "#categoryIds")
public Map<Long, SmErpCategoryEntity> getCategoryMap(Set<Long> categoryIds) {
// 查询逻辑
}
@Cacheable(value = "unitInfo", key = "#unitIds")
public Map<Long, SmErpUnitEntity> getUnitMap(Set<Long> unitIds) {
// 查询逻辑
}
}
```
### 3. 配置优化
#### 3.1 分页配置
```java
@Configuration
public class ProfitMarginConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("returnPageInfo", "check");
properties.setProperty("params", "count=countSql");
pageInterceptor.setProperties(properties);
return pageInterceptor;
}
}
```
#### 3.2 性能监控
```java
@Component
@Aspect
public class ProfitMarginPerformanceAspect {
@Around("execution(* com.bsd.pjcloud.supermarket.service.impl.ProfitMarginReportServiceImpl.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
log.info("方法 {} 执行耗时: {}ms", methodName, duration);
// 如果执行时间超过阈值,记录警告
if (duration > 5000) {
log.warn("方法 {} 执行时间过长: {}ms", methodName, duration);
}
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("方法 {} 执行失败,耗时: {}ms", methodName, endTime - startTime, e);
throw e;
}
}
}
```
### 4. 异常处理优化
#### 4.1 自定义异常
```java
public class ProfitMarginException extends BusinessException {
public ProfitMarginException(String message) {
super(message);
}
public ProfitMarginException(String message, Throwable cause) {
super(message, cause);
}
}
```
#### 4.2 统一异常处理
```java
@RestControllerAdvice
public class ProfitMarginExceptionHandler {
@ExceptionHandler(ProfitMarginException.class)
public Result<Void> handleProfitMarginException(ProfitMarginException e) {
log.error("毛利润报表异常", e);
return Result.fail(e.getMessage());
}
@ExceptionHandler(DataAccessException.class)
public Result<Void> handleDataAccessException(DataAccessException e) {
log.error("数据访问异常", e);
return Result.fail("数据查询失败,请稍后重试");
}
}
```
### 5. 测试优化
#### 5.1 单元测试
```java
@SpringBootTest
class ProfitMarginReportServiceTest {
@Autowired
private ProfitMarginReportService profitMarginReportService;
@Test
void testGetProfitMarginReport() {
// 准备测试数据
ProfitMarginReportQueryDTO queryDTO = new ProfitMarginReportQueryDTO();
queryDTO.setQueryType("day");
queryDTO.setStartDate(LocalDate.now().minusDays(7));
queryDTO.setEndDate(LocalDate.now());
// 执行测试
ProfitMarginReportVO result = profitMarginReportService.getProfitMarginReport(queryDTO);
// 验证结果
assertThat(result).isNotNull();
assertThat(result.getProfitSummary()).isNotNull();
assertThat(result.getProductProfitRanking()).isNotEmpty();
}
}
```
### 6. 主要优化点总结
1. **方法拆分**: 将大方法拆分为职责单一的小方法
2. **并行处理**: 使用CompletableFuture并行获取数据
3. **缓存优化**: 对频繁查询的数据进行缓存
4. **查询优化**: 使用关联查询减少数据库访问次数
5. **异常处理**: 统一异常处理和错误信息
6. **性能监控**: 添加性能监控和告警
7. **测试覆盖**: 完善单元测试和集成测试
这样重构后的代码将更加清晰、可维护,并且性能更好。
File diff suppressed because it is too large. Load diff
@@ -111,7 +111,16 @@
<el-input v-model="row.quantity" />
</template>
</vxe-column>
<vxe-column field="unitPrice" title="零售价">
<vxe-column field="costPrice" title="成本价">
<template #default="{ row }">
{{
row.costPrice
? displayPrice(row.costPrice)
: displayPrice(row.erpUnitVos?.find((item: any) => item.id == row.unitId)?.costPrice)
}}
</template>
</vxe-column>
<vxe-column field="retailPrice" title="零售价">
<template #default="{ row }">
{{
row.retailPrice