|
|
@@ -14,16 +14,25 @@ import org.springframework.util.StringUtils;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import shop.alien.entity.store.StoreBookingCategory;
|
|
|
import shop.alien.entity.store.StoreTable;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingTableGroupVo;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingTableReservationRowVo;
|
|
|
import shop.alien.entity.store.vo.StoreBookingTableVo;
|
|
|
+import shop.alien.entity.store.vo.UserReservationVo;
|
|
|
import shop.alien.mapper.StoreBookingTableMapper;
|
|
|
import shop.alien.mapper.UserReservationMapper;
|
|
|
import shop.alien.store.service.StoreBookingCategoryService;
|
|
|
import shop.alien.store.service.StoreBookingTableService;
|
|
|
import shop.alien.util.common.JwtUtil;
|
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
import java.util.Comparator;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -66,6 +75,7 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
|
|
|
.comparing(StoreTable::getCategoryId, Comparator.nullsLast(Integer::compareTo)) // 先按类别排序
|
|
|
.thenComparing(table -> parseTableNumberForSort(table.getTableNumber()))); // 再按桌号排序
|
|
|
|
|
|
+ log.info("getTableList 完成 storeId={} categoryId={} tableCount={}", storeId, categoryId, list.size());
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
@@ -89,7 +99,7 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
|
|
|
));
|
|
|
|
|
|
// 转换为VO并设置分类名称
|
|
|
- return tableList.stream()
|
|
|
+ List<StoreBookingTableVo> vos = tableList.stream()
|
|
|
.filter(table -> table != null)
|
|
|
.map(table -> {
|
|
|
StoreBookingTableVo vo = new StoreBookingTableVo();
|
|
|
@@ -102,6 +112,9 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
|
|
|
return vo;
|
|
|
})
|
|
|
.collect(Collectors.toList());
|
|
|
+ log.info("getTableListWithCategoryName 完成 storeId={} categoryId={} tableCount={} storeCategoryDefCount={}",
|
|
|
+ storeId, categoryId, vos.size(), categoryMap.size());
|
|
|
+ return vos;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -131,7 +144,227 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
|
|
|
|
|
|
return page;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 无分类桌号在分组 Map 中使用的占位 key;对外返回时分组 {@link StoreBookingTableGroupVo#getCategoryId()} 会转为 null。
|
|
|
+ */
|
|
|
+ private static final int UNCATEGORIZED_CATEGORY_GROUP_KEY = Integer.MIN_VALUE;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按桌号(桌子名称)子串模糊过滤;{@code tableName} 为空或仅空白时返回原列表。
|
|
|
+ */
|
|
|
+ private static List<StoreBookingTableVo> filterBookingTablesByNameLike(List<StoreBookingTableVo> vos, String tableName) {
|
|
|
+ if (!StringUtils.hasText(tableName)) {
|
|
|
+ return vos;
|
|
|
+ }
|
|
|
+ String needle = tableName.trim();
|
|
|
+ return vos.stream()
|
|
|
+ .filter(vo -> vo != null && vo.getTableNumber() != null && vo.getTableNumber().contains(needle))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<StoreBookingTableGroupVo> listTablesGroupedByMerchant(Integer merchantId, String tableName) {
|
|
|
+ if (merchantId == null) {
|
|
|
+ log.warn("listTablesGroupedByMerchant: merchantId 为空(应对应门店 storeId)");
|
|
|
+ }
|
|
|
+ log.info("listTablesGroupedByMerchant 开始 merchantId={} tableName={}", merchantId, tableName);
|
|
|
+ try {
|
|
|
+ // merchantId 在本项目中与门店 storeId 语义一致
|
|
|
+ List<StoreBookingTableVo> all = getTableListWithCategoryName(merchantId, null);
|
|
|
+ all = filterBookingTablesByNameLike(all, tableName);
|
|
|
+ Map<Integer, List<StoreBookingTableVo>> byCategory = groupTableVosByCategoryPreserveOrder(all);
|
|
|
+ Map<Integer, String> categoryNameMap = loadCategoryNamesForStoreAndGroupKeys(merchantId, byCategory.keySet());
|
|
|
+ Map<Integer, List<UserReservationVo>> reservationListByTableId = buildSortedReservationsByTableId(merchantId);
|
|
|
+ attachReservationsToTablesInCategoryMap(byCategory, reservationListByTableId);
|
|
|
+ List<StoreBookingTableGroupVo> result = buildStoreBookingTableGroupList(byCategory, categoryNameMap);
|
|
|
+ int reservationRowTotal = reservationListByTableId.values().stream().mapToInt(List::size).sum();
|
|
|
+ log.info(
|
|
|
+ "listTablesGroupedByMerchant 成功 merchantId={} tableCount={} categoryGroupCount={} responseGroupCount={} "
|
|
|
+ + "tablesWithReservations={} reservationRowTotal={}",
|
|
|
+ merchantId, all.size(), byCategory.size(), result.size(),
|
|
|
+ reservationListByTableId.size(), reservationRowTotal);
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("listTablesGroupedByMerchant 失败 merchantId={} tableName={}", merchantId, tableName, e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按分类 ID 分组桌号 VO:顺序与 {@link #getTableListWithCategoryName} 返回列表一致(LinkedHashMap 保序)。
|
|
|
+ * 无 {@code categoryId} 的桌统一归入占位 key {@link #UNCATEGORIZED_CATEGORY_GROUP_KEY},便于与真实分类 ID 区分。
|
|
|
+ */
|
|
|
+ private static Map<Integer, List<StoreBookingTableVo>> groupTableVosByCategoryPreserveOrder(List<StoreBookingTableVo> all) {
|
|
|
+ Map<Integer, List<StoreBookingTableVo>> byCategory = new LinkedHashMap<>();
|
|
|
+ for (StoreBookingTableVo vo : all) {
|
|
|
+ Integer key = vo.getCategoryId() != null ? vo.getCategoryId() : UNCATEGORIZED_CATEGORY_GROUP_KEY;
|
|
|
+ byCategory.computeIfAbsent(key, k -> new ArrayList<>()).add(vo);
|
|
|
+ }
|
|
|
+ log.info("groupTableVosByCategoryPreserveOrder 完成 tableCount={} categoryGroupCount={}", all.size(), byCategory.size());
|
|
|
+ return byCategory;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按分组用到的分类 ID 批量查询分类名称(不依赖桌 VO 上已带的 categoryName,以分类表为准)。
|
|
|
+ *
|
|
|
+ * @param storeId 门店 ID(与入参 merchantId 一致)
|
|
|
+ * @param categoryKeys {@link #groupTableVosByCategoryPreserveOrder} 的 key 集合,含占位 key 时会被过滤
|
|
|
+ */
|
|
|
+ private Map<Integer, String> loadCategoryNamesForStoreAndGroupKeys(Integer storeId, Set<Integer> categoryKeys) {
|
|
|
+ List<Integer> categoryIds = categoryKeys.stream()
|
|
|
+ .filter(id -> id != null && !Integer.valueOf(UNCATEGORIZED_CATEGORY_GROUP_KEY).equals(id))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (categoryIds.isEmpty()) {
|
|
|
+ log.info("loadCategoryNamesForStoreAndGroupKeys 跳过查询 storeId={} categoryKeysSize={} reason=无有效分类ID",
|
|
|
+ storeId, categoryKeys != null ? categoryKeys.size() : 0);
|
|
|
+ return new LinkedHashMap<>();
|
|
|
+ }
|
|
|
+ log.info("loadCategoryNamesForStoreAndGroupKeys 查询 storeId={} needCategoryIdCount={}", storeId, categoryIds.size());
|
|
|
+ log.debug("loadCategoryNamesForStoreAndGroupKeys categoryIds={}", categoryIds);
|
|
|
+ List<StoreBookingCategory> categories = storeBookingCategoryService.list(
|
|
|
+ new LambdaQueryWrapper<StoreBookingCategory>()
|
|
|
+ .eq(StoreBookingCategory::getStoreId, storeId)
|
|
|
+ .in(StoreBookingCategory::getId, categoryIds)
|
|
|
+ );
|
|
|
+ Map<Integer, String> map = categories.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ StoreBookingCategory::getId,
|
|
|
+ StoreBookingCategory::getCategoryName,
|
|
|
+ (v1, v2) -> v1
|
|
|
+ ));
|
|
|
+ if (map.size() != categoryIds.size()) {
|
|
|
+ log.warn("loadCategoryNamesForStoreAndGroupKeys 分类数量不一致 storeId={} 请求id数={} 实际查到={} 可能存在脏分类或未归属当前门店的分类",
|
|
|
+ storeId, categoryIds.size(), map.size());
|
|
|
+ } else {
|
|
|
+ log.info("loadCategoryNamesForStoreAndGroupKeys 完成 storeId={} loadedNameCount={}", storeId, map.size());
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询门店下预约与桌的关联行,聚合成「桌 ID → 该桌预约列表」。
|
|
|
+ * <p>
|
|
|
+ * 数据来源:{@link UserReservationMapper#listReservationRowsWithTableByStoreId},一行对应一条 (预约, 桌);
|
|
|
+ * {@code tableId} / 预约主键为空时跳过(无桌或脏数据)。同一桌同一预约按预约 ID 去重,避免联表重复行,
|
|
|
+ * 最后按 {@link #sortReservationsForTableView} 规则排序。
|
|
|
+ */
|
|
|
+ private Map<Integer, List<UserReservationVo>> buildSortedReservationsByTableId(Integer merchantId) {
|
|
|
+ log.info("buildSortedReservationsByTableId 开始 merchantId={}", merchantId);
|
|
|
+ List<StoreBookingTableReservationRowVo> reservationRows =
|
|
|
+ userReservationMapper.listReservationRowsWithTableByStoreId(merchantId);
|
|
|
+ Map<Integer, Map<Integer, UserReservationVo>> reservationsByTableId = new HashMap<>();
|
|
|
+ int skippedInvalid = 0;
|
|
|
+ for (StoreBookingTableReservationRowVo row : reservationRows) {
|
|
|
+ if (row.getTableId() == null || row.getId() == null) {
|
|
|
+ skippedInvalid++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ reservationsByTableId
|
|
|
+ .computeIfAbsent(row.getTableId(), k -> new LinkedHashMap<>())
|
|
|
+ .putIfAbsent(row.getId(), toReservationVo(row));
|
|
|
+ }
|
|
|
+ Map<Integer, List<UserReservationVo>> reservationListByTableId = new HashMap<>();
|
|
|
+ for (Map.Entry<Integer, Map<Integer, UserReservationVo>> e : reservationsByTableId.entrySet()) {
|
|
|
+ List<UserReservationVo> reservationList = new ArrayList<>(e.getValue().values());
|
|
|
+ sortReservationsForTableView(reservationList);
|
|
|
+ reservationListByTableId.put(e.getKey(), reservationList);
|
|
|
+ }
|
|
|
+ int reservationRowTotal = reservationListByTableId.values().stream().mapToInt(List::size).sum();
|
|
|
+ if (skippedInvalid > 0) {
|
|
|
+ log.warn("buildSortedReservationsByTableId 丢弃无效行 merchantId={} skippedInvalid={}(tableId或预约id为空)",
|
|
|
+ merchantId, skippedInvalid);
|
|
|
+ }
|
|
|
+ log.info(
|
|
|
+ "buildSortedReservationsByTableId 完成 merchantId={} sqlRowCount={} distinctTables={} distinctReservationLinks={}",
|
|
|
+ merchantId, reservationRows.size(), reservationListByTableId.size(), reservationRowTotal);
|
|
|
+ return reservationListByTableId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将每桌的预约列表写回 {@link StoreBookingTableVo#setReservations};无预约时置为空列表,避免前端收到 null。
|
|
|
+ * <p>
|
|
|
+ * 若该桌存在 {@link UserReservationVo#getStatus()} == 2(已到店)的预约,则将桌 {@link StoreBookingTableVo#setStatus}
|
|
|
+ * 置为 1(就餐中/已开台),并设置 {@link StoreBookingTableVo#setCurrentReservationId} 为其中最新到店的一条。
|
|
|
+ */
|
|
|
+ private static void attachReservationsToTablesInCategoryMap(
|
|
|
+ Map<Integer, List<StoreBookingTableVo>> byCategory,
|
|
|
+ Map<Integer, List<UserReservationVo>> reservationListByTableId) {
|
|
|
+ int tableTotal = 0;
|
|
|
+ int withArrivedReservation = 0;
|
|
|
+ for (List<StoreBookingTableVo> tables : byCategory.values()) {
|
|
|
+ for (StoreBookingTableVo tableVo : tables) {
|
|
|
+ tableTotal++;
|
|
|
+ List<UserReservationVo> reservations = reservationListByTableId.get(tableVo.getId());
|
|
|
+ if (reservations == null) {
|
|
|
+ reservations = Collections.emptyList();
|
|
|
+ }
|
|
|
+ tableVo.setReservations(reservations);
|
|
|
+ UserReservationVo arrived = pickLatestArrivedReservation(reservations);
|
|
|
+ if (arrived != null) {
|
|
|
+ withArrivedReservation++;
|
|
|
+ tableVo.setStatus(TABLE_STATUS_DINING);
|
|
|
+ tableVo.setCurrentReservationId(arrived.getId());
|
|
|
+ } else {
|
|
|
+ tableVo.setCurrentReservationId(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("attachReservationsToTablesInCategoryMap 完成 tableTotal={} withArrivedReservation={}",
|
|
|
+ tableTotal, withArrivedReservation);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final int RESERVATION_STATUS_ARRIVED = 2;
|
|
|
+ private static final int TABLE_STATUS_DINING = 1;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 多条已到店时取 {@code actual_arrival_time} 最新的一条,时间为空时按预约 id 较大者优先。
|
|
|
+ */
|
|
|
+ private static UserReservationVo pickLatestArrivedReservation(List<UserReservationVo> reservations) {
|
|
|
+ return reservations.stream()
|
|
|
+ .filter(r -> r.getStatus() != null && r.getStatus() == RESERVATION_STATUS_ARRIVED)
|
|
|
+ .max(Comparator
|
|
|
+ .comparing(UserReservationVo::getActualArrivalTime, Comparator.nullsFirst(Date::compareTo))
|
|
|
+ .thenComparing(UserReservationVo::getId, Comparator.nullsFirst(Integer::compareTo)))
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 由「分类 key → 桌列表」与分类名称映射组装 {@link StoreBookingTableGroupVo} 列表;占位 key 在 VO 中展示为 categoryId/categoryName 的 null。
|
|
|
+ */
|
|
|
+ private static List<StoreBookingTableGroupVo> buildStoreBookingTableGroupList(
|
|
|
+ Map<Integer, List<StoreBookingTableVo>> byCategory,
|
|
|
+ Map<Integer, String> categoryNameMap) {
|
|
|
+ List<StoreBookingTableGroupVo> result = new ArrayList<>(byCategory.size());
|
|
|
+ for (Map.Entry<Integer, List<StoreBookingTableVo>> e : byCategory.entrySet()) {
|
|
|
+ StoreBookingTableGroupVo group = new StoreBookingTableGroupVo();
|
|
|
+ Integer cid = e.getKey();
|
|
|
+ group.setCategoryId(cid == UNCATEGORIZED_CATEGORY_GROUP_KEY ? null : cid);
|
|
|
+ group.setTables(e.getValue());
|
|
|
+ group.setCategoryName(cid == UNCATEGORIZED_CATEGORY_GROUP_KEY ? null : categoryNameMap.get(cid));
|
|
|
+ result.add(group);
|
|
|
+ }
|
|
|
+ log.info("buildStoreBookingTableGroupList 完成 responseGroupCount={}", result.size());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static UserReservationVo toReservationVo(StoreBookingTableReservationRowVo row) {
|
|
|
+ UserReservationVo vo = new UserReservationVo();
|
|
|
+ BeanUtils.copyProperties(row, vo);
|
|
|
+ vo.setTableIds(Collections.singletonList(row.getTableId()));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同一桌多条预约:按预订日期倒序,再按开始时间正序
|
|
|
+ */
|
|
|
+ private static void sortReservationsForTableView(List<UserReservationVo> list) {
|
|
|
+ list.sort(Comparator
|
|
|
+ .comparing(UserReservationVo::getReservationDate, Comparator.nullsLast(Comparator.naturalOrder()))
|
|
|
+ .reversed()
|
|
|
+ .thenComparing(UserReservationVo::getStartTime, Comparator.nullsLast(String::compareTo)));
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 解析桌号用于排序
|
|
|
* 规则:字母(A-Z)优先,然后数字(由小到大)
|