成本价折线图

利润率柱状图
This commit is contained in:
2025-12-01 11:49:29 +08:00
parent e5c2c588d6
commit d7a8d6edc5
7 changed files with 349 additions and 36 deletions

View File

@@ -6,7 +6,9 @@ import com.czg.order.entity.ShopProdStatistic;
import com.czg.order.param.DataSummaryTradeParam;
import com.czg.order.service.ShopOrderStatisticService;
import com.czg.order.service.ShopProdStatisticService;
import com.czg.order.vo.CostLineChartVO;
import com.czg.order.vo.CountPayTypeVo;
import com.czg.order.vo.ProfitRateVO;
import com.czg.order.vo.TotalVo;
import com.czg.resp.CzgResult;
import com.czg.sa.StpKit;
@@ -71,7 +73,7 @@ public class DataSummaryController {
}
/**
* 销售趋势柱状图 左下
* 销售趋势柱状图 左下1
*
* @param day 天数
* @param shopId 店铺id
@@ -88,7 +90,7 @@ public class DataSummaryController {
}
/**
* 支付占比饼图 左下
* 支付占比饼图 左下2
*
* @param day 天数
* @param shopId 店铺id
@@ -102,4 +104,38 @@ public class DataSummaryController {
List<CountPayTypeVo> data = orderStatisticService.getSummaryPayTypeData(shopId, day);
return CzgResult.success(data);
}
/**
* 毛利率/净利率 柱状图 左下下
* 利润率柱状图
* @param day 天数
* @param shopId 店铺id
*/
@GetMapping("profitRateBarChart")
@SaAdminCheckPermission(value = "dataSummary:profitRateBarChart", name = "毛利率/净利率柱状图 左下下")
public CzgResult<List<ProfitRateVO>> profitRateBarChart(@RequestParam Integer day, @RequestParam(required = false) Long shopId) {
AssertUtil.isNull(day, "天数不能为空");
if (shopId == null) {
shopId = StpKit.USER.getShopId();
}
List<ProfitRateVO> data = orderStatisticService.profitRateBarChart(shopId, day);
return CzgResult.success(data);
}
/**
* 成本折线图 右下下
* @param day 天数
* @param shopId 店铺id
*/
@GetMapping("costLineChart")
@SaAdminCheckPermission(value = "dataSummary:costLineChart", name = "成本折线图 右下下")
public CzgResult<List<CostLineChartVO>> costLineChart(@RequestParam Integer day, @RequestParam(required = false) Long shopId) {
AssertUtil.isNull(day, "天数不能为空");
if (shopId == null) {
shopId = StpKit.USER.getShopId();
}
List<CostLineChartVO> data = orderStatisticService.costLineChart(shopId, day);
return CzgResult.success(data);
}
}

View File

@@ -1,6 +1,8 @@
package com.czg.order.service;
import com.czg.order.vo.CostLineChartVO;
import com.czg.order.vo.CountPayTypeVo;
import com.czg.order.vo.ProfitRateVO;
import com.czg.order.vo.TotalVo;
import com.mybatisflex.core.service.IService;
import com.czg.order.entity.ShopOrderStatistic;
@@ -40,6 +42,16 @@ public interface ShopOrderStatisticService extends IService<ShopOrderStatistic>
* 获取支付方式数据
*/
List<CountPayTypeVo> getSummaryPayTypeData(Long shopId, Integer day);
/**
* 利润率折线图
*/
List<ProfitRateVO> profitRateBarChart(Long shopId, Integer day);
/**
* 成本折线图
*/
List<CostLineChartVO> costLineChart(Long shopId, Integer day);

View File

@@ -0,0 +1,73 @@
package com.czg.order.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
/**
* 成本折线图 左下下
*
* @author ww
*/
@Data
@NoArgsConstructor
public class CostLineChartVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 成本价
*/
private BigDecimal productCostAmount;
/**
* 日期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate tradeDay;
public CostLineChartVO(LocalDate tradeDay) {
this.tradeDay = tradeDay;
}
/**
* 合并实时数据和历史数据,并填充缺失的日期
*/
public static List<CostLineChartVO> mergeAndFillData(CostLineChartVO onlineData, List<CostLineChartVO> historyData,
LocalDate startDate, LocalDate endDate) {
// 创建日期到数据的映射,方便查找
Map<LocalDate, CostLineChartVO> dataMap = new HashMap<>();
// 将历史数据放入映射
for (CostLineChartVO vo : historyData) {
if (vo.getTradeDay() != null) {
dataMap.put(vo.getTradeDay(), vo);
}
}
dataMap.put(onlineData.getTradeDay(), onlineData);
List<CostLineChartVO> result = new ArrayList<>();
LocalDate currentDay = startDate;
while (!currentDay.isAfter(endDate)) {
if (dataMap.containsKey(currentDay)) {
result.add(dataMap.get(currentDay));
} else {
// 创建空的TotalVo填充缺失的日期
CostLineChartVO emptyVo = new CostLineChartVO(currentDay);
result.add(emptyVo);
}
currentDay = currentDay.plusDays(1);
}
// 按日期排序确保顺序正确
result.sort(Comparator.comparing(CostLineChartVO::getTradeDay));
return result;
}
}

View File

@@ -40,6 +40,21 @@ public class CountPayTypeVo {
PAY_TYPE_MAPPING.put("creditPay", "挂账支付");
}
/**
* 实时数据
*/
public static List<CountPayTypeVo> realTimeDataByDay(Map<String, BigDecimal> realTimeData) {
List<CountPayTypeVo> result = new ArrayList<>();
for (Map.Entry<String, String> entry : PAY_TYPE_MAPPING.entrySet()) {
String payCode = entry.getKey();
String payName = entry.getValue();
BigDecimal totalCount = getSafeValue(realTimeData, payCode);
result.add(new CountPayTypeVo(totalCount.intValue(), payName));
}
return result;
}
/**
* 合并实时数据和历史统计数据

View File

@@ -0,0 +1,79 @@
package com.czg.order.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
/**
* 毛利率/净利率 柱状图 左下下
* 利润率柱状图
*
* @author ww
*/
@Data
@NoArgsConstructor
public class ProfitRateVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 毛利率
*/
private BigDecimal profitRate;
/**
* 净利润率
*/
private BigDecimal netProfitRate;
/**
* 日期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate tradeDay;
public ProfitRateVO(LocalDate tradeDay) {
this.tradeDay = tradeDay;
}
/**
* 合并实时数据和历史数据,并填充缺失的日期
*/
public static List<ProfitRateVO> mergeAndFillData(ProfitRateVO onlineData, List<ProfitRateVO> historyData,
LocalDate startDate, LocalDate endDate) {
// 创建日期到数据的映射,方便查找
Map<LocalDate, ProfitRateVO> dataMap = new HashMap<>();
// 将历史数据放入映射
for (ProfitRateVO vo : historyData) {
if (vo.getTradeDay() != null) {
dataMap.put(vo.getTradeDay(), vo);
}
}
dataMap.put(onlineData.getTradeDay(), onlineData);
List<ProfitRateVO> result = new ArrayList<>();
LocalDate currentDay = startDate;
while (!currentDay.isAfter(endDate)) {
if (dataMap.containsKey(currentDay)) {
result.add(dataMap.get(currentDay));
} else {
// 创建空的TotalVo填充缺失的日期
ProfitRateVO emptyVo = new ProfitRateVO(currentDay);
result.add(emptyVo);
}
currentDay = currentDay.plusDays(1);
}
// 按日期排序确保顺序正确
result.sort(Comparator.comparing(ProfitRateVO::getTradeDay));
return result;
}
}

View File

@@ -1,7 +1,9 @@
package com.czg.service.order.mapper;
import com.czg.order.entity.ShopOrderStatistic;
import com.czg.order.vo.CostLineChartVO;
import com.czg.order.vo.ProductCostAmountVO;
import com.czg.order.vo.ProfitRateVO;
import com.czg.order.vo.TotalVo;
import com.mybatisflex.core.BaseMapper;
import org.apache.ibatis.annotations.Select;
@@ -141,6 +143,31 @@ public interface ShopOrderStatisticMapper extends BaseMapper<ShopOrderStatistic>
Map<String, BigDecimal> getPayTypeDateRangeRaw(Long shopId, LocalDate start, LocalDate end);
@Select("SELECT" +
" profit_rate as profitRate, " +
" net_profit_rate as netProfitRate, " +
" statistic_date as tradeDay" +
" FROM" +
" tb_shop_order_statistic " +
" WHERE" +
" shop_id = #{shopId} " +
"and statistic_date >= #{start} " +
"and statistic_date <= #{end} ")
List<ProfitRateVO> profitRateBarChart(Long shopId, LocalDate start, LocalDate end);
@Select("SELECT" +
" product_cost_amount as productCostAmount, " +
" statistic_date as tradeDay" +
" FROM" +
" tb_shop_order_statistic " +
" WHERE" +
" shop_id = #{shopId} " +
"and statistic_date >= #{start} " +
"and statistic_date <= #{end} ")
List<CostLineChartVO> costLineChart(Long shopId, LocalDate start, LocalDate end);
//*********************以下为日常统计********************************************************************************

View File

@@ -6,9 +6,7 @@ import cn.hutool.core.collection.CollUtil;
import com.czg.exception.CzgException;
import com.czg.order.entity.ShopOrderStatistic;
import com.czg.order.service.ShopOrderStatisticService;
import com.czg.order.vo.CountPayTypeVo;
import com.czg.order.vo.ProductCostAmountVO;
import com.czg.order.vo.TotalVo;
import com.czg.order.vo.*;
import com.czg.service.order.mapper.ShopOrderStatisticMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
@@ -65,23 +63,56 @@ public class ShopOrderStatisticServiceImpl extends ServiceImpl<ShopOrderStatisti
@Override
public List<TotalVo> getDateAmount(Long shopId, Integer day) {
LocalDate currentDate = LocalDate.now();
LocalDate startDate = currentDate;
if (day == 7) {
startDate = currentDate.minusDays(6);
} else if (day == 30) {
startDate = currentDate.minusDays(29);
}
LocalDate startDate;
TotalVo onlineDataAmount = mapper.getOnlineDataAmount(shopId, currentDate);
if (onlineDataAmount == null) {
onlineDataAmount = new TotalVo(currentDate);
}
if (day <= 1) {
return List.of(onlineDataAmount);
}else {
startDate = currentDate.minusDays(day - 1);
}
List<TotalVo> statDateRange = mapper.getStatDateRange(shopId, startDate, currentDate);
return TotalVo.mergeAndFillData(onlineDataAmount, statDateRange, startDate, currentDate);
}
@Override
public List<CountPayTypeVo> getSummaryPayTypeData(Long shopId, Integer day) {
LocalDate currentDate = LocalDate.now();
LocalDate startDate;
if (day <= 1) {
Map<String, BigDecimal> onlinePayTypeDate = mapper.getOnlinePayTypeDate(shopId, currentDate);
return CountPayTypeVo.realTimeDataByDay(onlinePayTypeDate);
}else {
startDate = currentDate.minusDays(day - 1);
}
return CountPayTypeVo.mergePayTypeData(
mapper.getOnlinePayTypeDate(shopId, currentDate),
mapper.getPayTypeDateRangeRaw(shopId, startDate, currentDate));
}
@Override
public List<ProfitRateVO> profitRateBarChart(Long shopId, Integer day) {
LocalDate currentDate = LocalDate.now();
LocalDate startDate;
ShopOrderStatistic onlineDataAmount = getRealTimeDataByDay(shopId, currentDate);
ProfitRateVO onlineProfitRateBarChart = new ProfitRateVO(currentDate);
if (onlineDataAmount != null) {
onlineProfitRateBarChart.setProfitRate(onlineDataAmount.getProfitRate());
onlineProfitRateBarChart.setNetProfitRate(onlineDataAmount.getNetProfitRate());
}
if (day <= 1) {
return List.of(onlineProfitRateBarChart);
}else {
startDate = currentDate.minusDays(day - 1);
}
List<ProfitRateVO> statDateRange = mapper.profitRateBarChart(shopId, startDate, currentDate);
return ProfitRateVO.mergeAndFillData(onlineProfitRateBarChart, statDateRange, startDate, currentDate);
}
@Override
public List<CostLineChartVO> costLineChart(Long shopId, Integer day) {
LocalDate currentDate = LocalDate.now();
LocalDate startDate = currentDate;
if (day == 7) {
@@ -89,11 +120,17 @@ public class ShopOrderStatisticServiceImpl extends ServiceImpl<ShopOrderStatisti
} else if (day == 30) {
startDate = currentDate.minusDays(29);
}
return CountPayTypeVo.mergePayTypeData(
mapper.getOnlinePayTypeDate(shopId, currentDate),
mapper.getPayTypeDateRangeRaw(shopId, startDate, currentDate));
CostLineChartVO onlineCostLineChart = new CostLineChartVO(currentDate);
BigDecimal productCostAmount = getProductCostAmount(shopId, currentDate);
onlineCostLineChart.setProductCostAmount(productCostAmount);
List<CostLineChartVO> statDateRange = mapper.costLineChart(shopId, startDate, currentDate);
return CostLineChartVO.mergeAndFillData(onlineCostLineChart, statDateRange, startDate, currentDate);
}
//u--------------------------------------------------_---------------------------------------------------------
@Override
public void statisticAndInsert(Long shopId, LocalDate day) {
ShopOrderStatistic realTimeData = getRealTimeDataByDay(shopId, day);
@@ -185,7 +222,7 @@ public class ShopOrderStatisticServiceImpl extends ServiceImpl<ShopOrderStatisti
}
/**
* 获取商品成本价
* 获取商品成本价
*/
private BigDecimal getProductCostAmount(Long shopId, LocalDate day) {
BigDecimal productCostAmount = BigDecimal.ZERO;
@@ -223,43 +260,77 @@ public class ShopOrderStatisticServiceImpl extends ServiceImpl<ShopOrderStatisti
return productCostAmount;
}
/**
* 统一入口方法
* 计算 客单价 翻台率 净利润 净利率 毛利润 毛利率
*/
private void calculateShopOrderStatistic(ShopOrderStatistic result) {
//毛利润(订单实付金额-商品成本)
calculateProfitInfo(result);
calculateAvgPayAmount(result);
calculateTurnoverRate(result);
}
/**
* 计算毛利润、毛利率(净利润、净利率与之一致)
*/
private void calculateProfitInfo(ShopOrderStatistic result) {
// 初始化商品成本金额避免null
if (result.getProductCostAmount() == null) {
result.setProductCostAmount(BigDecimal.ZERO);
}
result.setProfitAmount(result.getPayAmount().subtract(result.getProductCostAmount()));
//毛利(订单实付金额-商品成本)/订单实付金额*100%
// 计算毛利(订单实付金额-商品成本)
BigDecimal profitAmount = result.getPayAmount().subtract(result.getProductCostAmount());
result.setProfitAmount(profitAmount);
// 计算毛利率((订单实付金额-商品成本)/订单实付金额*100%
BigDecimal profitRate = BigDecimal.ZERO;
if (result.getPayAmount().compareTo(BigDecimal.ZERO) > 0) {
BigDecimal profitRate = result.getProfitAmount().divide(result.getPayAmount(), 4, RoundingMode.HALF_DOWN).multiply(BigDecimal.valueOf(100));
result.setProfitRate(profitRate);
} else {
result.setProfitRate(BigDecimal.ZERO);
profitRate = profitAmount.divide(result.getPayAmount(), 4, RoundingMode.HALF_DOWN)
.multiply(BigDecimal.valueOf(100));
}
//净利润 净利率 目前和 毛利润 毛利率 一致
result.setNetProfitRate(result.getProfitRate());
result.setNetProfitAmount(result.getProfitAmount());
//客单价 实付金额(包括线上支付 包含现金支付 包含会员支付 包含挂账)/就餐人数
result.setProfitRate(profitRate);
// 净利润、净利率目前和毛利润、毛利率一致
result.setNetProfitAmount(profitAmount);
result.setNetProfitRate(profitRate);
}
/**
* 计算客单价
* 客单价 = 实付金额 / 就餐人数就餐人数为0时直接使用实付金额
*/
private void calculateAvgPayAmount(ShopOrderStatistic result) {
BigDecimal avgPayAmount;
// 校验就餐人数有效性
if (result.getCustomerCount() != null && result.getCustomerCount() > 0) {
result.setAvgPayAmount(result.getPayAmount().divide(new BigDecimal(result.getCustomerCount()), 2, RoundingMode.HALF_DOWN));
avgPayAmount = result.getPayAmount().divide(
new BigDecimal(result.getCustomerCount()), 2, RoundingMode.HALF_DOWN
);
} else {
result.setAvgPayAmount(result.getPayAmount());
avgPayAmount = result.getPayAmount();
}
//翻台率 (订单数-桌台数)/桌台数*100%
result.setAvgPayAmount(avgPayAmount);
}
/**
* 计算翻台率
* 翻台率 = (订单数-桌台数)/桌台数*100%差值≤0时翻台率为0
*/
private void calculateTurnoverRate(ShopOrderStatistic result) {
BigDecimal turnoverRate = BigDecimal.ZERO;
// 校验桌台数有效性
if (result.getTableCount() != null && result.getTableCount() > 0) {
long orderTableDifference = result.getOrderCount() - result.getTableCount();
BigDecimal turnoverRate = BigDecimal.ZERO;
// 仅当差值大于0时计算翻台率
if (orderTableDifference > 0) {
turnoverRate = new BigDecimal(orderTableDifference).divide(new BigDecimal(result.getTableCount()), 2, RoundingMode.HALF_DOWN);
turnoverRate = new BigDecimal(orderTableDifference)
.divide(new BigDecimal(result.getTableCount()), 2, RoundingMode.HALF_DOWN);
}
result.setTurnoverRate(turnoverRate);
} else {
result.setTurnoverRate(BigDecimal.ZERO);
}
result.setTurnoverRate(turnoverRate);
}