Răsfoiți Sursa

维护查询桌号用餐状态 补充桌号名称
维护填写用餐信息接口

lutong 3 săptămâni în urmă
părinte
comite
5a1ad288c5

+ 1 - 1
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -50,7 +50,7 @@ public class DiningController {
         }
     }
 
-    @ApiOperation(value = "查询餐桌是否处于就餐中", notes = "免登录可调用,用于前端判断是否跳过选择用餐人数。就餐中(status=1)、加餐(status=3) 均视为就餐状态,且 diner_count 有值 时 inDining=true")
+    @ApiOperation(value = "查询餐桌是否处于就餐中", notes = "免登录可调用,用于前端判断是否跳过选择用餐人数。返回 tableNumber 为桌号名称。就餐中(status=1)、加餐(status=3) 均视为就餐状态,且 diner_count 有值 时 inDining=true")
     @GetMapping("/table-dining-status")
     public R<TableDiningStatusVO> getTableDiningStatus(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {

+ 7 - 3
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java

@@ -48,18 +48,22 @@ public class DiningServiceImpl implements DiningService {
     @Override
     public TableDiningStatusVO getTableDiningStatus(Integer tableId) {
         if (tableId == null) {
-            return new TableDiningStatusVO(null, false, null);
+            return new TableDiningStatusVO(null, null, false, null);
         }
         StoreTable table = storeTableMapper.selectById(tableId);
         if (table == null) {
-            return new TableDiningStatusVO(tableId, false, null);
+            return new TableDiningStatusVO(tableId, null, false, null);
         }
         // 就餐中(1)、加餐(3) 均视为就餐状态;且需已有就餐人数
         Integer status = table.getStatus();
         boolean inDining = (Integer.valueOf(1).equals(status) || Integer.valueOf(3).equals(status))
                 && table.getDinerCount() != null && table.getDinerCount() > 0;
         Integer dinerCount = inDining ? table.getDinerCount() : null;
-        return new TableDiningStatusVO(tableId, inDining, dinerCount);
+        String tableNumber = table.getTableNumber() != null ? table.getTableNumber().trim() : null;
+        if (tableNumber != null && tableNumber.isEmpty()) {
+            tableNumber = null;
+        }
+        return new TableDiningStatusVO(tableId, tableNumber, inDining, dinerCount);
     }
 
     @Override

+ 121 - 2
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningWalkInReservationServiceImpl.java

@@ -1,5 +1,6 @@
 package shop.alien.dining.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -16,9 +17,19 @@ import shop.alien.mapper.UserReservationMapper;
 import shop.alien.mapper.UserReservationTableMapper;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.Date;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
 
 /**
  * 点餐流程:填写就餐信息 → 仅写 user_reservation + user_reservation_table,状态已到店,不产生 user_reservation_order
@@ -33,6 +44,11 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
     private final UserReservationTableMapper userReservationTableMapper;
 
     private static final int STATUS_ARRIVED = 2;
+    /** 占桌状态:与其它预订冲突检测范围内 */
+    private static final ZoneId SHANGHAI = ZoneId.of("Asia/Shanghai");
+    /** 未填结束时间时,按 N 小时作为占用窗口做冲突判断(与订金预订「结束时间」空值场景对齐) */
+    private static final int DEFAULT_BLOCK_HOURS_WHEN_NO_END = 4;
+    private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm");
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -47,10 +63,17 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
 
         Date reservationDate = dto.getReservationDate();
         if (reservationDate == null) {
-            ZoneId shanghai = ZoneId.of("Asia/Shanghai");
-            reservationDate = Date.from(LocalDate.now(shanghai).atStartOfDay(shanghai).toInstant());
+            reservationDate = Date.from(LocalDate.now(SHANGHAI).atStartOfDay(SHANGHAI).toInstant());
         }
 
+        LocalDate day = reservationDate.toInstant().atZone(SHANGHAI).toLocalDate();
+        LocalTime newStartLt = parseHm(dto.getStartTime());
+        if (newStartLt == null) {
+            throw new RuntimeException("开始时间格式不正确,请使用 HH:mm");
+        }
+        LocalTime newEndLt = parseHm(dto.getEndTime());
+        assertNoTableBookingConflict(table.getId(), table.getStoreId(), day, newStartLt, newEndLt);
+
         Date now = new Date();
         UserReservation entity = new UserReservation();
         entity.setReservationNo(generateReservationNo());
@@ -98,4 +121,100 @@ public class DiningWalkInReservationServiceImpl extends ServiceImpl<UserReservat
     private static String generateReservationNo() {
         return "RV" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
     }
+
+    /**
+     * 与其它有效预订(同桌、同日、状态为待确认/已确认/已到店)时段是否重叠;重叠则不允许到店就餐。
+     */
+    private void assertNoTableBookingConflict(Integer tableId, Integer storeId, LocalDate day,
+                                              LocalTime newStart, LocalTime newEnd) {
+        List<UserReservationTable> links = userReservationTableMapper.selectList(
+                new LambdaQueryWrapper<UserReservationTable>()
+                        .eq(UserReservationTable::getTableId, tableId)
+                        .eq(UserReservationTable::getDeleteFlag, 0));
+        if (links == null || links.isEmpty()) {
+            return;
+        }
+        Set<Integer> resIds = links.stream()
+                .map(UserReservationTable::getReservationId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(HashSet::new));
+        if (resIds.isEmpty()) {
+            return;
+        }
+        LocalDateTime newStartLdt = LocalDateTime.of(day, newStart);
+        LocalDateTime newEndLdt = windowEnd(day, newStart, newEnd);
+
+        List<UserReservation> others = new ArrayList<>(this.listByIds(resIds));
+        if (others.isEmpty()) {
+            return;
+        }
+        for (UserReservation other : others) {
+            if (other == null || !storeId.equals(other.getStoreId())) {
+                continue;
+            }
+            if (!isOccupyingBookingStatus(other.getStatus())) {
+                continue;
+            }
+            LocalDate otherDay = toLocalDate(other.getReservationDate());
+            if (otherDay == null || !day.equals(otherDay)) {
+                continue;
+            }
+            LocalTime oStart = parseHm(other.getStartTime());
+            if (oStart == null) {
+                continue;
+            }
+            LocalTime oEnd = parseHm(other.getEndTime());
+            LocalDateTime oStartLdt = LocalDateTime.of(otherDay, oStart);
+            LocalDateTime oEndLdt = windowEnd(otherDay, oStart, oEnd);
+            if (newStartLdt.isBefore(oEndLdt) && oStartLdt.isBefore(newEndLdt)) {
+                log.warn("到店就餐信息与既有预订冲突 tableId={} day={} new=[{} - {}] otherResId={} other=[{} - {}]",
+                        tableId, day, newStartLdt, newEndLdt, other.getId(), oStartLdt, oEndLdt);
+                throw new RuntimeException("该时段与已有预订冲突,无法就餐");
+            }
+        }
+    }
+
+    /** 待确认、已确认、已到店视为占用桌位;已取消、未到店超时、用餐结束不占用 */
+    private static boolean isOccupyingBookingStatus(Integer status) {
+        return status != null && (status == 0 || status == 1 || status == 2);
+    }
+
+    private static LocalDate toLocalDate(Date d) {
+        if (d == null) {
+            return null;
+        }
+        return d.toInstant().atZone(SHANGHAI).toLocalDate();
+    }
+
+    private static LocalTime parseHm(String raw) {
+        if (!StringUtils.hasText(raw)) {
+            return null;
+        }
+        String s = raw.trim();
+        try {
+            return LocalTime.parse(s, HM);
+        } catch (DateTimeParseException e) {
+            try {
+                return LocalTime.parse(s, DateTimeFormatter.ofPattern("H:mm"));
+            } catch (DateTimeParseException e2) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * 预约结束时刻:结束早于或等于开始(同一张日历日上的钟点)则视为跨日至次日;结束时间为空则用默认时长。
+     */
+    private static LocalDateTime windowEnd(LocalDate day, LocalTime start, LocalTime end) {
+        if (end == null) {
+            return LocalDateTime.of(day, start).plusHours(DEFAULT_BLOCK_HOURS_WHEN_NO_END);
+        }
+        if (end.isAfter(start)) {
+            return LocalDateTime.of(day, end);
+        }
+        if (end.equals(start)) {
+            return LocalDateTime.of(day, start).plusHours(DEFAULT_BLOCK_HOURS_WHEN_NO_END);
+        }
+        return LocalDateTime.of(day.plusDays(1), end);
+    }
 }

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/dto/DiningWalkInReservationDTO.java

@@ -35,7 +35,7 @@ public class DiningWalkInReservationDTO {
     @NotBlank(message = "开始时间不能为空")
     private String startTime;
 
-    @ApiModelProperty(value = "结束时间 HH:mm,可次日,选填")
+    @ApiModelProperty(value = "结束时间 HH:mm;若早于开始时间则视为次日结束。选填,不填则按4小时占用窗口参与冲突校验")
     private String endTime;
 
     @ApiModelProperty(value = "就餐人姓名")

+ 4 - 1
alien-entity/src/main/java/shop/alien/entity/store/vo/TableDiningStatusVO.java

@@ -16,7 +16,7 @@ import java.io.Serializable;
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-@ApiModel(value = "TableDiningStatusVO", description = "餐桌就餐状态(是否就餐中、就餐人数)")
+@ApiModel(value = "TableDiningStatusVO", description = "餐桌就餐状态(桌号文案、是否就餐中、就餐人数)")
 public class TableDiningStatusVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
@@ -24,6 +24,9 @@ public class TableDiningStatusVO implements Serializable {
     @ApiModelProperty(value = "桌号ID")
     private Integer tableId;
 
+    @ApiModelProperty(value = "桌号名称(如 A01;桌不存在或未查到则为 null)")
+    private String tableNumber;
+
     @ApiModelProperty(value = "是否处于就餐中(true=就餐中且已有就餐人数数据)")
     private Boolean inDining;
 

+ 13 - 0
alien-store/src/main/java/shop/alien/store/controller/dining/StoreDiningPathProxyController.java

@@ -6,10 +6,12 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.DiningWalkInReservationDTO;
 import shop.alien.entity.store.vo.*;
 import shop.alien.store.feign.DiningServiceFeign;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
 import java.util.List;
 
 /**
@@ -44,6 +46,17 @@ public class StoreDiningPathProxyController {
         }
     }
 
+    @ApiOperation("提交到店就餐信息(写入预约表,已到店;返回预约ID供下单绑定)")
+    @PostMapping("/walk-in/reservation")
+    public R<Integer> createWalkInReservation(HttpServletRequest request, @Valid @RequestBody DiningWalkInReservationDTO dto) {
+        try {
+            return diningServiceFeign.createWalkInReservation(auth(request), dto);
+        } catch (Exception e) {
+            log.error("createWalkInReservation: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
     @ApiOperation("获取点餐页面信息")
     @GetMapping("/page-info")
     public R<DiningPageInfoVO> getDiningPageInfo(

+ 12 - 1
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -17,6 +17,7 @@ import shop.alien.entity.store.dto.AddCartItemDTO;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.ChangeTableDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.dto.DiningWalkInReservationDTO;
 import shop.alien.entity.store.dto.UpdateOrderCouponDTO;
 import shop.alien.entity.store.vo.*;
 import shop.alien.store.feign.dto.DiningChangePhoneBody;
@@ -44,12 +45,22 @@ public interface DiningServiceFeign {
      * 查询餐桌是否处于就餐中(免登录可调用)
      *
      * @param tableId 桌号ID
-     * @return R.data 为 TableDiningStatusVO(inDining、dinerCount)
+     * @return R.data 为 TableDiningStatusVO(tableNumber、inDining、dinerCount)
      */
     @GetMapping("/store/dining/table-dining-status")
     R<TableDiningStatusVO> getTableDiningStatus(@RequestParam("tableId") Integer tableId);
 
     /**
+     * 提交到店就餐信息(写入 user_reservation,状态已到店),需登录
+     *
+     * @return R.data 为 user_reservation.id
+     */
+    @PostMapping("/store/dining/walk-in/reservation")
+    R<Integer> createWalkInReservation(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestBody DiningWalkInReservationDTO dto);
+
+    /**
      * 获取点餐页面信息
      *
      * @param authorization 请求头 Authorization,供 dining 解析当前用户