|
@@ -1,16 +1,17 @@
|
|
|
package com.mdframe.forge.business.core.hotel.order.service.impl;
|
|
package com.mdframe.forge.business.core.hotel.order.service.impl;
|
|
|
|
|
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.mdframe.forge.business.core.hotel.constant.HotelConfigKeys;
|
|
import com.mdframe.forge.business.core.hotel.constant.HotelConfigKeys;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.domain.HotelBusinessHours;
|
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDish;
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDish;
|
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDishAddition;
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDishAddition;
|
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDishSpecOption;
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelDishSpecOption;
|
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelRoom;
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelRoom;
|
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelRoomStay;
|
|
import com.mdframe.forge.business.core.hotel.domain.HotelRoomStay;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.mapper.HotelBusinessHoursMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelConfigMapper;
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelConfigMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelDishAdditionMapper;
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelDishAdditionMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelDishMapper;
|
|
import com.mdframe.forge.business.core.hotel.mapper.HotelDishMapper;
|
|
@@ -25,6 +26,7 @@ import com.mdframe.forge.business.core.hotel.order.mapper.HotelOrderItemMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.order.mapper.HotelOrderMapper;
|
|
import com.mdframe.forge.business.core.hotel.order.mapper.HotelOrderMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.notification.manager.SseEmitterManager;
|
|
import com.mdframe.forge.business.core.hotel.notification.manager.SseEmitterManager;
|
|
|
import com.mdframe.forge.business.core.hotel.order.service.HotelOrderService;
|
|
import com.mdframe.forge.business.core.hotel.order.service.HotelOrderService;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.order.vo.DashboardVO;
|
|
|
import com.mdframe.forge.business.core.hotel.order.vo.HotelOrderVO;
|
|
import com.mdframe.forge.business.core.hotel.order.vo.HotelOrderVO;
|
|
|
import com.mdframe.forge.business.core.hotel.order.vo.OrderDashboardVO;
|
|
import com.mdframe.forge.business.core.hotel.order.vo.OrderDashboardVO;
|
|
|
import com.mdframe.forge.business.core.hotel.pay.service.HotelPayService;
|
|
import com.mdframe.forge.business.core.hotel.pay.service.HotelPayService;
|
|
@@ -39,12 +41,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.util.StringUtils;
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.text.SimpleDateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
import java.util.Arrays;
|
|
|
import java.util.Calendar;
|
|
import java.util.Calendar;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
import java.util.Objects;
|
|
import java.util.Objects;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
@@ -79,6 +83,9 @@ public class HotelOrderServiceImpl implements HotelOrderService {
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private HotelConfigMapper configMapper;
|
|
private HotelConfigMapper configMapper;
|
|
|
|
|
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private HotelBusinessHoursMapper businessHoursMapper;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 支付服务(单向依赖:HotelPayService 不注入本服务,不构成循环)。
|
|
* 支付服务(单向依赖:HotelPayService 不注入本服务,不构成循环)。
|
|
|
* 用于拒单/退单流转时自动发起退款。
|
|
* 用于拒单/退单流转时自动发起退款。
|
|
@@ -115,8 +122,8 @@ public class HotelOrderServiceImpl implements HotelOrderService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
- public OrderDashboardVO orderDashboard() {
|
|
|
|
|
- OrderDashboardVO vo = orderMapper.selectDashboard(resolveTenantId());
|
|
|
|
|
|
|
+ public OrderDashboardVO orderDashboard(Date queryDate) {
|
|
|
|
|
+ OrderDashboardVO vo = orderMapper.selectDashboard(resolveTenantId(), queryDate);
|
|
|
if (vo == null) {
|
|
if (vo == null) {
|
|
|
vo = new OrderDashboardVO();
|
|
vo = new OrderDashboardVO();
|
|
|
vo.setPendingCount(0);
|
|
vo.setPendingCount(0);
|
|
@@ -128,6 +135,118 @@ public class HotelOrderServiceImpl implements HotelOrderService {
|
|
|
return vo;
|
|
return vo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 营业数据看板(独立页面) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public DashboardVO dashboard() {
|
|
|
|
|
+ Long tenantId = resolveTenantId();
|
|
|
|
|
+ DashboardVO vo = new DashboardVO();
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 1. 今日KPI(含环比昨日) ----
|
|
|
|
|
+ Map<String, Object> kpiMap = orderMapper.selectDashboardKpi(tenantId);
|
|
|
|
|
+ DashboardVO.TodayKpiVO kpi = new DashboardVO.TodayKpiVO();
|
|
|
|
|
+ if (kpiMap != null) {
|
|
|
|
|
+ int todayOrders = toInt(kpiMap.get("todayOrderCount"));
|
|
|
|
|
+ int yesterdayOrders = toInt(kpiMap.get("yesterdayOrderCount"));
|
|
|
|
|
+ BigDecimal todayRevenue = toBigDecimal(kpiMap.get("todayRevenue"));
|
|
|
|
|
+ BigDecimal yesterdayRevenue = toBigDecimal(kpiMap.get("yesterdayRevenue"));
|
|
|
|
|
+ int todayAvgDelivery = toInt(kpiMap.get("todayAvgDelivery"));
|
|
|
|
|
+ int yesterdayAvgDelivery = toInt(kpiMap.get("yesterdayAvgDelivery"));
|
|
|
|
|
+ int todayRefundCount = toInt(kpiMap.get("todayRefundCount"));
|
|
|
|
|
+ int yesterdayRefundCount = toInt(kpiMap.get("yesterdayRefundCount"));
|
|
|
|
|
+
|
|
|
|
|
+ kpi.setOrderCount(todayOrders);
|
|
|
|
|
+ kpi.setOrderCountGrowth(calcGrowth(todayOrders, yesterdayOrders));
|
|
|
|
|
+ kpi.setRevenue(todayRevenue);
|
|
|
|
|
+ kpi.setRevenueGrowth(calcGrowthDecimal(todayRevenue, yesterdayRevenue));
|
|
|
|
|
+ kpi.setAvgDeliveryMinutes(todayAvgDelivery);
|
|
|
|
|
+ kpi.setAvgDeliveryGrowth(calcGrowth(todayAvgDelivery, yesterdayAvgDelivery));
|
|
|
|
|
+
|
|
|
|
|
+ double todayRefundRate = todayOrders > 0 ? (double) todayRefundCount / todayOrders : 0.0;
|
|
|
|
|
+ double yesterdayRefundRate = yesterdayOrders > 0 ? (double) yesterdayRefundCount / yesterdayOrders : 0.0;
|
|
|
|
|
+ kpi.setRefundRate(todayRefundRate);
|
|
|
|
|
+ kpi.setRefundRateGrowth(yesterdayRefundRate > 0
|
|
|
|
|
+ ? (todayRefundRate - yesterdayRefundRate) / yesterdayRefundRate
|
|
|
|
|
+ : null);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ kpi.setOrderCount(0);
|
|
|
|
|
+ kpi.setOrderCountGrowth(null);
|
|
|
|
|
+ kpi.setRevenue(BigDecimal.ZERO);
|
|
|
|
|
+ kpi.setRevenueGrowth(null);
|
|
|
|
|
+ kpi.setAvgDeliveryMinutes(0);
|
|
|
|
|
+ kpi.setAvgDeliveryGrowth(null);
|
|
|
|
|
+ kpi.setRefundRate(0.0);
|
|
|
|
|
+ kpi.setRefundRateGrowth(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setTodayKpi(kpi);
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 2. 近7天订单趋势 ----
|
|
|
|
|
+ List<Map<String, Object>> trendRows = orderMapper.selectLast7DaysTrend(tenantId);
|
|
|
|
|
+ List<DashboardVO.DailyTrendVO> trendList = new ArrayList<>();
|
|
|
|
|
+ SimpleDateFormat dayFmt = new SimpleDateFormat("MM/dd");
|
|
|
|
|
+ if (trendRows != null) {
|
|
|
|
|
+ for (Map<String, Object> row : trendRows) {
|
|
|
|
|
+ DashboardVO.DailyTrendVO trend = new DashboardVO.DailyTrendVO();
|
|
|
|
|
+ Object statDate = row.get("statDate");
|
|
|
|
|
+ if (statDate instanceof java.sql.Date) {
|
|
|
|
|
+ trend.setDate(dayFmt.format(new Date(((java.sql.Date) statDate).getTime())));
|
|
|
|
|
+ } else if (statDate != null) {
|
|
|
|
|
+ trend.setDate(statDate.toString().length() >= 10
|
|
|
|
|
+ ? statDate.toString().substring(5, 7) + "/" + statDate.toString().substring(8, 10)
|
|
|
|
|
+ : statDate.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+ trend.setOrderCount(toInt(row.get("orderCount")));
|
|
|
|
|
+ trend.setRevenue(toBigDecimal(row.get("revenue")));
|
|
|
|
|
+ trendList.add(trend);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setTrend(trendList);
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 3. 本周热门菜品Top5 ----
|
|
|
|
|
+ List<Map<String, Object>> dishRows = orderMapper.selectWeeklyHotDishes(tenantId);
|
|
|
|
|
+ List<DashboardVO.HotDishVO> hotDishList = new ArrayList<>();
|
|
|
|
|
+ if (dishRows != null) {
|
|
|
|
|
+ int rank = 1;
|
|
|
|
|
+ for (Map<String, Object> row : dishRows) {
|
|
|
|
|
+ DashboardVO.HotDishVO dish = new DashboardVO.HotDishVO();
|
|
|
|
|
+ dish.setRank(rank++);
|
|
|
|
|
+ dish.setDishName(row.get("dishName") != null ? row.get("dishName").toString() : "");
|
|
|
|
|
+ dish.setSalesCount(toInt(row.get("salesCount")));
|
|
|
|
|
+ hotDishList.add(dish);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setHotDishes(hotDishList);
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 4. 今日餐段订单分布(从营业时段表获取餐段时间范围) ----
|
|
|
|
|
+ List<HotelBusinessHours> hoursList = businessHoursMapper.selectEnabled(tenantId);
|
|
|
|
|
+ List<DashboardVO.MealDistributionVO> mealList = new ArrayList<>();
|
|
|
|
|
+ // 获取当前时间 HH:mm,用于判断当前处于哪个餐段
|
|
|
|
|
+ SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm");
|
|
|
|
|
+ String nowTime = timeFmt.format(new Date());
|
|
|
|
|
+ if (hoursList != null) {
|
|
|
|
|
+ for (HotelBusinessHours bh : hoursList) {
|
|
|
|
|
+ DashboardVO.MealDistributionVO meal = new DashboardVO.MealDistributionVO();
|
|
|
|
|
+ meal.setMealType(bh.getMealType());
|
|
|
|
|
+ meal.setMealName(bh.getMealName());
|
|
|
|
|
+ // 根据餐段时间范围统计今日订单数
|
|
|
|
|
+ if (bh.getStartTime() != null && bh.getEndTime() != null) {
|
|
|
|
|
+ int count = orderMapper.countOrdersByMealTime(tenantId, bh.getStartTime(), bh.getEndTime());
|
|
|
|
|
+ meal.setOrderCount(count);
|
|
|
|
|
+ // 判断当前是否处于该餐段营业时间内
|
|
|
|
|
+ meal.setIsInProgress(nowTime.compareTo(bh.getStartTime()) >= 0
|
|
|
|
|
+ && nowTime.compareTo(bh.getEndTime()) < 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ meal.setOrderCount(0);
|
|
|
|
|
+ meal.setIsInProgress(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ mealList.add(meal);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setMealDistribution(mealList);
|
|
|
|
|
+
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ==================== 创建订单 ====================
|
|
// ==================== 创建订单 ====================
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -617,34 +736,22 @@ public class HotelOrderServiceImpl implements HotelOrderService {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 生成订单号: RM + YYYYMMDD + 4位序号。
|
|
* 生成订单号: RM + YYYYMMDD + 4位序号。
|
|
|
- * 使用重试机制应对并发冲突。
|
|
|
|
|
|
|
+ * selectMaxOrderNo 已查询所有行(含逻辑删除),确保序号单调递增、永不回收,
|
|
|
|
|
+ * 已放弃/已删除的订单号不会被新订单复用,支持未来恢复场景。
|
|
|
*/
|
|
*/
|
|
|
private String generateOrderNo() {
|
|
private String generateOrderNo() {
|
|
|
String dateStr = DATE_FMT.format(new Date());
|
|
String dateStr = DATE_FMT.format(new Date());
|
|
|
String prefix = "RM" + dateStr;
|
|
String prefix = "RM" + dateStr;
|
|
|
- for (int retry = 0; retry < 3; retry++) {
|
|
|
|
|
- String maxNo = orderMapper.selectMaxOrderNo(prefix);
|
|
|
|
|
- int seq = 1;
|
|
|
|
|
- if (maxNo != null && maxNo.length() > prefix.length()) {
|
|
|
|
|
- try {
|
|
|
|
|
- seq = Integer.parseInt(maxNo.substring(prefix.length())) + 1;
|
|
|
|
|
- } catch (NumberFormatException ignored) {
|
|
|
|
|
- // 解析失败使用默认序号1
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- String orderNo = prefix + String.format("%04d", seq);
|
|
|
|
|
- // 检查是否已存在(防御性检查)
|
|
|
|
|
- QueryWrapper<HotelOrder> wrapper = new QueryWrapper<HotelOrder>();
|
|
|
|
|
- wrapper.eq("order_no", orderNo);
|
|
|
|
|
- Long count = orderMapper.selectCount(wrapper);
|
|
|
|
|
- if (count == null || count == 0) {
|
|
|
|
|
- return orderNo;
|
|
|
|
|
|
|
+ String maxNo = orderMapper.selectMaxOrderNo(prefix);
|
|
|
|
|
+ int seq = 1;
|
|
|
|
|
+ if (maxNo != null && maxNo.length() > prefix.length()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ seq = Integer.parseInt(maxNo.substring(prefix.length())) + 1;
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ // 解析失败使用默认序号1
|
|
|
}
|
|
}
|
|
|
- // 已存在则重试
|
|
|
|
|
- log.warn("订单号冲突,重试生成: {}", orderNo);
|
|
|
|
|
}
|
|
}
|
|
|
- // 3次重试后使用时间戳后缀
|
|
|
|
|
- return prefix + String.format("%04d", (int) (System.currentTimeMillis() % 10000));
|
|
|
|
|
|
|
+ return prefix + String.format("%04d", seq);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -736,4 +843,60 @@ public class HotelOrderServiceImpl implements HotelOrderService {
|
|
|
.filter(Objects::nonNull)
|
|
.filter(Objects::nonNull)
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全转换 Map 结果中的数值为 int(null/非数字返回0)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private int toInt(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Number) {
|
|
|
|
|
+ return ((Number) value).intValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.parseInt(value.toString());
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全转换 Map 结果中的数值为 BigDecimal(null返回0)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal toBigDecimal(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof BigDecimal) {
|
|
|
|
|
+ return (BigDecimal) value;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(value.toString());
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return BigDecimal.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算环比增长率(整数版):(today - yesterday) / yesterday。
|
|
|
|
|
+ * yesterday为0或null时返回null(无意义)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Double calcGrowth(int today, int yesterday) {
|
|
|
|
|
+ if (yesterday == 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return (double) (today - yesterday) / yesterday;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算环比增长率(BigDecimal版):(today - yesterday) / yesterday。
|
|
|
|
|
+ * yesterday为0或null时返回null。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Double calcGrowthDecimal(BigDecimal today, BigDecimal yesterday) {
|
|
|
|
|
+ if (yesterday == null || yesterday.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return today.subtract(yesterday).divide(yesterday, 4, RoundingMode.HALF_UP).doubleValue();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|