فهرست منبع

预订信息设置 关联营业时间

qinxuyang 1 ماه پیش
والد
کامیت
93349dd128
15فایلهای تغییر یافته به همراه1115 افزوده شده و 53 حذف شده
  1. 82 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingBusinessHours.java
  2. 16 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreBookingSettings.java
  3. 43 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingBusinessHoursDTO.java
  4. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingSettingsDTO.java
  5. 13 0
      alien-entity/src/main/java/shop/alien/mapper/StoreBookingBusinessHoursMapper.java
  6. 30 0
      alien-entity/src/main/resources/mapper/StoreBookingBusinessHoursMapper.xml
  7. 5 0
      alien-entity/src/main/resources/mapper/StoreBookingSettingsMapper.xml
  8. 222 0
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingBusinessHoursController.java
  9. 68 23
      alien-store/src/main/java/shop/alien/store/controller/StoreBookingSettingsController.java
  10. 57 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingBusinessHoursService.java
  11. 17 0
      alien-store/src/main/java/shop/alien/store/service/StoreBookingSettingsService.java
  12. 237 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingBusinessHoursServiceImpl.java
  13. 13 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingCategoryServiceImpl.java
  14. 223 27
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java
  15. 69 3
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingTableServiceImpl.java

+ 82 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingBusinessHours.java

@@ -0,0 +1,82 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 预订服务营业时间表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("store_booking_business_hours")
+@ApiModel(value = "StoreBookingBusinessHours对象", description = "预订服务营业时间表")
+public class StoreBookingBusinessHours {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "关联store_booking_settings表 id")
+    @TableField("settings_id")
+    private Integer settingsId;
+
+    @ApiModelProperty(value = "营业类型(0:正常营业, 1:特殊营业/节假日)")
+    @TableField("business_type")
+    private Integer businessType;
+
+    @ApiModelProperty(value = "节假日类型(当business_type=1时使用,如:春节、元旦、国庆节等,为空表示正常营业)")
+    @TableField("holiday_type")
+    private String holidayType;
+
+    @ApiModelProperty(value = "节假日日期(当business_type=1时使用,用于指定具体日期,可选,格式:yyyy-MM-dd)")
+    @TableField("holiday_date")
+    private String holidayDate;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)")
+    @TableField("booking_time_type")
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "开始时间(HH:mm格式,非全天时必填)")
+    @TableField("start_time")
+    private String startTime;
+
+    @ApiModelProperty(value = "结束时间(HH:mm格式,非全天时必填)")
+    @TableField("end_time")
+    private String endTime;
+
+    @ApiModelProperty(value = "排序(用于同一门店多条记录的排序)")
+    @TableField("sort")
+    private Integer sort;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 16 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreBookingSettings.java

@@ -57,6 +57,22 @@ public class StoreBookingSettings {
     @TableField("max_capacity_per_slot")
     private Integer maxCapacityPerSlot;
 
+    @ApiModelProperty(value = "预约(0:免费, 1:付费)")
+    @TableField("reservation")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额(元)")
+    @TableField("reservation_money")
+    private Double reservationMoney;
+
+    @ApiModelProperty(value = "取消预订退费时长设置,需提前(小时)")
+    @TableField("off_unsubscribe_hours")
+    private Integer offUnsubscribeHours;
+
+    @ApiModelProperty(value = "营业时间结束前多少分钟不可预订")
+    @TableField("booking_not_available_time")
+    private Integer bookingNotAvailableTime;
+
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic

+ 43 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingBusinessHoursDTO.java

@@ -0,0 +1,43 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 预订服务营业时间DTO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@ApiModel(value = "StoreBookingBusinessHoursDTO", description = "预订服务营业时间DTO")
+public class StoreBookingBusinessHoursDTO {
+
+    @ApiModelProperty(value = "营业时间ID(编辑时必填)")
+    private Integer id;
+
+    @ApiModelProperty(value = "关联store_booking_settings表 id", required = true)
+    private Integer settingsId;
+
+    @ApiModelProperty(value = "营业类型(0:正常营业, 1:特殊营业/节假日)", required = true)
+    private Integer businessType;
+
+    @ApiModelProperty(value = "节假日类型(当business_type=1时使用,如:春节、元旦、国庆节等)")
+    private String holidayType;
+
+    @ApiModelProperty(value = "节假日日期(当business_type=1时使用,用于指定具体日期,可选,格式:yyyy-MM-dd)")
+    private String holidayDate;
+
+    @ApiModelProperty(value = "预订时间类型(0:非全天, 1:全天)", required = true)
+    private Integer bookingTimeType;
+
+    @ApiModelProperty(value = "开始时间(HH:mm格式,非全天时必填)")
+    private String startTime;
+
+    @ApiModelProperty(value = "结束时间(HH:mm格式,非全天时必填)")
+    private String endTime;
+
+    @ApiModelProperty(value = "排序(用于同一门店多条记录的排序)")
+    private Integer sort;
+}

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingSettingsDTO.java

@@ -4,6 +4,8 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 预订服务信息设置DTO
  *
@@ -40,4 +42,22 @@ public class StoreBookingSettingsDTO {
 
     @ApiModelProperty(value = "单时段最大容纳人数", required = true)
     private Integer maxCapacityPerSlot;
+
+    @ApiModelProperty(value = "预约(0:免费, 1:付费)")
+    private String reservation;
+
+    @ApiModelProperty(value = "预订金额(元)")
+    private Double reservationMoney;
+
+    @ApiModelProperty(value = "取消预订退费时长设置,需提前(小时)", required = true)
+    private Integer offUnsubscribeHours;
+
+    @ApiModelProperty(value = "营业时间结束前多少分钟不可预订", required = true)
+    private Integer bookingNotAvailableTime;
+
+    @ApiModelProperty(value = "正常营业时间(business_type=0)")
+    private StoreBookingBusinessHoursDTO normalBusinessHours;
+
+    @ApiModelProperty(value = "特殊营业时间列表(business_type=1,节假日营业时间)")
+    private List<StoreBookingBusinessHoursDTO> specialBusinessHoursList;
 }

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreBookingBusinessHoursMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+
+/**
+ * 预订服务营业时间表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingBusinessHoursMapper extends BaseMapper<StoreBookingBusinessHours> {
+}

+ 30 - 0
alien-entity/src/main/resources/mapper/StoreBookingBusinessHoursMapper.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.StoreBookingBusinessHoursMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.StoreBookingBusinessHours">
+        <id column="id" property="id" />
+        <result column="settings_id" property="settingsId" />
+        <result column="business_type" property="businessType" />
+        <result column="holiday_type" property="holidayType" />
+        <result column="holiday_date" property="holidayDate" />
+        <result column="booking_time_type" property="bookingTimeType" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="sort" property="sort" />
+        <result column="delete_flag" property="deleteFlag" />
+        <result column="created_time" property="createdTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_time" property="updatedTime" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, settings_id, business_type, holiday_type, holiday_date, 
+        booking_time_type, start_time, end_time, sort, 
+        delete_flag, created_time, created_user_id, updated_time, updated_user_id
+    </sql>
+
+</mapper>

+ 5 - 0
alien-entity/src/main/resources/mapper/StoreBookingSettingsMapper.xml

@@ -13,6 +13,10 @@
         <result column="booking_start_time" property="bookingStartTime" />
         <result column="booking_end_time" property="bookingEndTime" />
         <result column="max_capacity_per_slot" property="maxCapacityPerSlot" />
+        <result column="reservation" property="reservation" />
+        <result column="reservation_money" property="reservationMoney" />
+        <result column="off_unsubscribe_hours" property="offUnsubscribeHours" />
+        <result column="booking_not_available_time" property="bookingNotAvailableTime" />
         <result column="delete_flag" property="deleteFlag" />
         <result column="created_time" property="createdTime" />
         <result column="created_user_id" property="createdUserId" />
@@ -24,6 +28,7 @@
     <sql id="Base_Column_List">
         id, store_id, retain_position_flag, retention_duration, booking_date_display_days, 
         booking_time_type, booking_start_time, booking_end_time, max_capacity_per_slot, 
+        reservation, reservation_money, off_unsubscribe_hours, booking_not_available_time, 
         delete_flag, created_time, created_user_id, updated_time, updated_user_id
     </sql>
 

+ 222 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingBusinessHoursController.java

@@ -0,0 +1,222 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 预订服务营业时间管理 前端控制器
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Api(tags = {"预订服务-营业时间管理"})
+@ApiSort(18)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/business-hours")
+@RequiredArgsConstructor
+public class StoreBookingBusinessHoursController {
+
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("查询预订服务营业时间列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "settingsId", value = "设置ID(关联store_booking_settings表 id)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "businessType", value = "营业类型(0:正常营业, 1:特殊营业/节假日,可选,不传则查询全部)", dataType = "Integer", paramType = "query", required = false)
+    })
+    @GetMapping("/list")
+    public R<List<StoreBookingBusinessHours>> getBusinessHoursList(
+            @RequestParam Integer settingsId,
+            @RequestParam(required = false) Integer businessType) {
+        log.info("StoreBookingBusinessHoursController.getBusinessHoursList?settingsId={}, businessType={}", settingsId, businessType);
+        
+        if (settingsId == null) {
+            return R.fail("设置ID不能为空");
+        }
+        
+        try {
+            List<StoreBookingBusinessHours> list;
+            if (businessType != null) {
+                list = storeBookingBusinessHoursService.getListBySettingsIdAndType(settingsId, businessType);
+            } else {
+                list = storeBookingBusinessHoursService.getListBySettingsId(settingsId);
+            }
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询预订服务营业时间列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("查询预订服务营业时间详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "营业时间ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreBookingBusinessHours> getBusinessHoursDetail(@RequestParam Integer id) {
+        log.info("StoreBookingBusinessHoursController.getBusinessHoursDetail?id={}", id);
+        
+        if (id == null) {
+            return R.fail("营业时间ID不能为空");
+        }
+        
+        try {
+            StoreBookingBusinessHours businessHours = storeBookingBusinessHoursService.getById(id);
+            if (businessHours == null) {
+                return R.fail("营业时间不存在");
+            }
+            return R.data(businessHours);
+        } catch (Exception e) {
+            log.error("查询预订服务营业时间详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("新增或更新预订服务营业时间")
+    @PostMapping("/save")
+    public R<String> saveBusinessHours(@RequestBody StoreBookingBusinessHoursDTO dto) {
+        log.info("StoreBookingBusinessHoursController.saveBusinessHours?dto={}", dto);
+        
+        // 参数验证
+        if (dto.getSettingsId() == null) {
+            return R.fail("设置ID不能为空");
+        }
+        if (dto.getBusinessType() == null) {
+            return R.fail("营业类型不能为空");
+        }
+        if (dto.getBookingTimeType() == null) {
+            return R.fail("预订时间类型不能为空");
+        }
+        
+        // 如果是特殊营业(节假日),验证节假日类型
+        if (dto.getBusinessType() != null && dto.getBusinessType() == 1) {
+            if (!StringUtils.hasText(dto.getHolidayType())) {
+                return R.fail("特殊营业时必须填写节假日类型");
+            }
+        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+        if (dto.getBookingTimeType() != null && dto.getBookingTimeType() == 0) {
+            if (!StringUtils.hasText(dto.getStartTime())) {
+                return R.fail("非全天时必须填写开始时间");
+            }
+            if (!StringUtils.hasText(dto.getEndTime())) {
+                return R.fail("非全天时必须填写结束时间");
+            }
+        }
+        
+        try {
+            StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+            businessHours.setId(dto.getId());
+            businessHours.setSettingsId(dto.getSettingsId());
+            businessHours.setBusinessType(dto.getBusinessType());
+            businessHours.setHolidayType(dto.getHolidayType());
+            businessHours.setHolidayDate(dto.getHolidayDate());
+            businessHours.setBookingTimeType(dto.getBookingTimeType());
+            businessHours.setStartTime(dto.getStartTime());
+            businessHours.setEndTime(dto.getEndTime());
+            businessHours.setSort(dto.getSort());
+            
+            boolean result = storeBookingBusinessHoursService.saveOrUpdateBusinessHours(businessHours);
+            if (result) {
+                return R.success("保存成功");
+            } else {
+                return R.fail("保存失败");
+            }
+        } catch (Exception e) {
+            log.error("保存预订服务营业时间失败", e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("批量保存预订服务营业时间")
+    @PostMapping("/batch-save")
+    public R<String> batchSaveBusinessHours(@RequestBody List<StoreBookingBusinessHoursDTO> dtoList) {
+        log.info("StoreBookingBusinessHoursController.batchSaveBusinessHours?size={}", dtoList != null ? dtoList.size() : 0);
+        
+        if (dtoList == null || dtoList.isEmpty()) {
+            return R.fail("营业时间列表不能为空");
+        }
+        
+        // 验证所有DTO的settingsId是否一致
+        Integer settingsId = dtoList.get(0).getSettingsId();
+        if (settingsId == null) {
+            return R.fail("设置ID不能为空");
+        }
+        for (StoreBookingBusinessHoursDTO dto : dtoList) {
+            if (dto.getSettingsId() == null || !dto.getSettingsId().equals(settingsId)) {
+                return R.fail("所有营业时间的设置ID必须一致");
+            }
+        }
+        
+        try {
+            // 转换为实体对象列表
+            List<StoreBookingBusinessHours> businessHoursList = dtoList.stream()
+                    .map(dto -> {
+                        StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+                        businessHours.setId(dto.getId());
+                        businessHours.setSettingsId(dto.getSettingsId());
+                        businessHours.setBusinessType(dto.getBusinessType());
+                        businessHours.setHolidayType(dto.getHolidayType());
+                        businessHours.setHolidayDate(dto.getHolidayDate());
+                        businessHours.setBookingTimeType(dto.getBookingTimeType());
+                        businessHours.setStartTime(dto.getStartTime());
+                        businessHours.setEndTime(dto.getEndTime());
+                        businessHours.setSort(dto.getSort());
+                        return businessHours;
+                    })
+                    .collect(Collectors.toList());
+            
+            boolean result = storeBookingBusinessHoursService.batchSaveBusinessHours(settingsId, businessHoursList);
+            if (result) {
+                return R.success("批量保存成功");
+            } else {
+                return R.fail("批量保存失败");
+            }
+        } catch (Exception e) {
+            log.error("批量保存预订服务营业时间失败", e);
+            return R.fail("批量保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("删除预订服务营业时间")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "营业时间ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<String> deleteBusinessHours(@RequestParam Integer id) {
+        log.info("StoreBookingBusinessHoursController.deleteBusinessHours?id={}", id);
+        
+        if (id == null) {
+            return R.fail("营业时间ID不能为空");
+        }
+        
+        try {
+            boolean result = storeBookingBusinessHoursService.deleteBusinessHours(id);
+            if (result) {
+                return R.success("删除成功");
+            } else {
+                return R.fail("删除失败");
+            }
+        } catch (Exception e) {
+            log.error("删除预订服务营业时间失败", e);
+            return R.fail("删除失败:" + e.getMessage());
+        }
+    }
+}

+ 68 - 23
alien-store/src/main/java/shop/alien/store/controller/StoreBookingSettingsController.java

@@ -6,7 +6,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
-import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
 import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
 import shop.alien.store.service.StoreBookingSettingsService;
 
@@ -28,12 +28,12 @@ public class StoreBookingSettingsController {
     private final StoreBookingSettingsService storeBookingSettingsService;
 
     @ApiOperationSupport(order = 1)
-    @ApiOperation("查询预订服务信息设置")
+    @ApiOperation("查询预订服务信息设置(包含营业时间)")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
     })
     @GetMapping("/detail")
-    public R<StoreBookingSettings> getSettings(@RequestParam Integer storeId) {
+    public R<StoreBookingSettingsDTO> getSettings(@RequestParam Integer storeId) {
         log.info("StoreBookingSettingsController.getSettings?storeId={}", storeId);
         
         if (storeId == null) {
@@ -41,11 +41,11 @@ public class StoreBookingSettingsController {
         }
         
         try {
-            StoreBookingSettings settings = storeBookingSettingsService.getByStoreId(storeId);
-            if (settings == null) {
+            StoreBookingSettingsDTO dto = storeBookingSettingsService.getSettingsWithBusinessHoursByStoreId(storeId);
+            if (dto == null) {
                 return R.fail("未找到该门店的预订服务信息设置");
             }
-            return R.data(settings);
+            return R.data(dto);
         } catch (Exception e) {
             log.error("查询预订服务信息设置失败", e);
             return R.fail("查询失败:" + e.getMessage());
@@ -53,7 +53,7 @@ public class StoreBookingSettingsController {
     }
 
     @ApiOperationSupport(order = 2)
-    @ApiOperation("保存预订服务信息设置(新增或更新)")
+    @ApiOperation("保存预订服务信息设置(新增或更新,包含营业时间)")
     @PostMapping("/save")
     public R<String> saveSettings(@RequestBody StoreBookingSettingsDTO dto) {
         log.info("StoreBookingSettingsController.saveSettings?dto={}", dto);
@@ -78,28 +78,73 @@ public class StoreBookingSettingsController {
             return R.fail("单时段最大容纳人数必须大于0");
         }
         
+        // 验证预订相关字段
+        // 如果选择付费(reservation=1),必须填写预订金额
+        if (StringUtils.hasText(dto.getReservation()) && "1".equals(dto.getReservation())) {
+            if (dto.getReservationMoney() == null || dto.getReservationMoney() < 0) {
+                return R.fail("选择付费时必须填写预订金额,且金额不能小于0");
+            }
+        }
+        
+        // 验证取消预订退费时长设置
+        if (dto.getOffUnsubscribeHours() == null || dto.getOffUnsubscribeHours() < 0) {
+            return R.fail("取消预订退费时长设置必须大于等于0");
+        }
+        
+        // 验证营业时间结束前不可预订时间
+        if (dto.getBookingNotAvailableTime() == null || dto.getBookingNotAvailableTime() < 0) {
+            return R.fail("营业时间结束前不可预订时间必须大于等于0");
+        }
+        
         // 如果选择非全天,必须填写开始时间和结束时间
-        if (dto.getBookingTimeType() != null && dto.getBookingTimeType() == 0) {
-            if (!StringUtils.hasText(dto.getBookingStartTime())) {
-                return R.fail("非全天时必须填写开始时间");
+//        if (dto.getBookingTimeType() != null && dto.getBookingTimeType() == 0) {
+//            if (!StringUtils.hasText(dto.getBookingStartTime())) {
+//                return R.fail("非全天时必须填写开始时间");
+//            }
+//            if (!StringUtils.hasText(dto.getBookingEndTime())) {
+//                return R.fail("非全天时必须填写结束时间");
+//            }
+//        }
+        
+        // 验证正常营业时间
+        if (dto.getNormalBusinessHours() != null) {
+            StoreBookingBusinessHoursDTO normalHours = dto.getNormalBusinessHours();
+            if (normalHours.getBookingTimeType() == null) {
+                return R.fail("正常营业时间的预订时间类型不能为空");
+            }
+            if (normalHours.getBookingTimeType() == 0) {
+                if (!StringUtils.hasText(normalHours.getStartTime())) {
+                    return R.fail("正常营业时间非全天时必须填写开始时间");
+                }
+                if (!StringUtils.hasText(normalHours.getEndTime())) {
+                    return R.fail("正常营业时间非全天时必须填写结束时间");
+                }
             }
-            if (!StringUtils.hasText(dto.getBookingEndTime())) {
-                return R.fail("非全天时必须填写结束时间");
+        }
+        
+        // 验证特殊营业时间列表
+        if (dto.getSpecialBusinessHoursList() != null && !dto.getSpecialBusinessHoursList().isEmpty()) {
+            for (StoreBookingBusinessHoursDTO specialHours : dto.getSpecialBusinessHoursList()) {
+                if (!StringUtils.hasText(specialHours.getHolidayType())) {
+                    return R.fail("特殊营业时间必须填写节假日类型");
+                }
+                if (specialHours.getBookingTimeType() == null) {
+                    return R.fail("特殊营业时间的预订时间类型不能为空");
+                }
+                if (specialHours.getBookingTimeType() == 0) {
+                    if (!StringUtils.hasText(specialHours.getStartTime())) {
+                        return R.fail("特殊营业时间非全天时必须填写开始时间");
+                    }
+                    if (!StringUtils.hasText(specialHours.getEndTime())) {
+                        return R.fail("特殊营业时间非全天时必须填写结束时间");
+                    }
+                }
             }
         }
         
         try {
-            StoreBookingSettings settings = new StoreBookingSettings();
-            settings.setStoreId(dto.getStoreId());
-            settings.setRetainPositionFlag(dto.getRetainPositionFlag());
-            settings.setRetentionDuration(dto.getRetentionDuration());
-            settings.setBookingDateDisplayDays(dto.getBookingDateDisplayDays());
-            settings.setBookingTimeType(dto.getBookingTimeType());
-            settings.setBookingStartTime(dto.getBookingStartTime());
-            settings.setBookingEndTime(dto.getBookingEndTime());
-            settings.setMaxCapacityPerSlot(dto.getMaxCapacityPerSlot());
-            
-            boolean result = storeBookingSettingsService.saveOrUpdateSettings(settings);
+            // 使用新的方法保存设置和营业时间
+            boolean result = storeBookingSettingsService.saveSettingsWithBusinessHours(dto);
             if (result) {
                 return R.success("保存成功");
             } else {

+ 57 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingBusinessHoursService.java

@@ -0,0 +1,57 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+
+import java.util.List;
+
+/**
+ * 预订服务营业时间表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface StoreBookingBusinessHoursService extends IService<StoreBookingBusinessHours> {
+
+    /**
+     * 根据设置ID查询营业时间列表
+     *
+     * @param settingsId 设置ID
+     * @return 营业时间列表
+     */
+    List<StoreBookingBusinessHours> getListBySettingsId(Integer settingsId);
+
+    /**
+     * 根据设置ID和营业类型查询营业时间列表
+     *
+     * @param settingsId 设置ID
+     * @param businessType 营业类型(0:正常营业, 1:特殊营业/节假日)
+     * @return 营业时间列表
+     */
+    List<StoreBookingBusinessHours> getListBySettingsIdAndType(Integer settingsId, Integer businessType);
+
+    /**
+     * 新增或更新营业时间
+     *
+     * @param businessHours 营业时间对象
+     * @return boolean
+     */
+    boolean saveOrUpdateBusinessHours(StoreBookingBusinessHours businessHours);
+
+    /**
+     * 批量保存营业时间
+     *
+     * @param settingsId 设置ID
+     * @param businessHoursList 营业时间列表
+     * @return boolean
+     */
+    boolean batchSaveBusinessHours(Integer settingsId, List<StoreBookingBusinessHours> businessHoursList);
+
+    /**
+     * 删除营业时间(逻辑删除)
+     *
+     * @param id 营业时间ID
+     * @return boolean
+     */
+    boolean deleteBusinessHours(Integer id);
+}

+ 17 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingSettingsService.java

@@ -2,6 +2,7 @@ package shop.alien.store.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
 
 /**
  * 预订服务信息设置表 服务类
@@ -26,4 +27,20 @@ public interface StoreBookingSettingsService extends IService<StoreBookingSettin
      * @return boolean
      */
     boolean saveOrUpdateSettings(StoreBookingSettings settings);
+
+    /**
+     * 保存预订服务信息设置(包含营业时间)
+     *
+     * @param dto 设置DTO(包含营业时间信息)
+     * @return boolean
+     */
+    boolean saveSettingsWithBusinessHours(StoreBookingSettingsDTO dto);
+
+    /**
+     * 根据门店ID查询预订服务信息设置(包含营业时间)
+     *
+     * @param storeId 门店ID
+     * @return StoreBookingSettingsDTO
+     */
+    StoreBookingSettingsDTO getSettingsWithBusinessHoursByStoreId(Integer storeId);
 }

+ 237 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingBusinessHoursServiceImpl.java

@@ -0,0 +1,237 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreBookingBusinessHours;
+import shop.alien.mapper.StoreBookingBusinessHoursMapper;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * 预订服务营业时间表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreBookingBusinessHoursServiceImpl extends ServiceImpl<StoreBookingBusinessHoursMapper, StoreBookingBusinessHours> implements StoreBookingBusinessHoursService {
+
+    // 时间格式正则:HH:mm
+    private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3]):[0-5][0-9]$");
+
+    @Override
+    public List<StoreBookingBusinessHours> getListBySettingsId(Integer settingsId) {
+        log.info("StoreBookingBusinessHoursServiceImpl.getListBySettingsId?settingsId={}", settingsId);
+        
+        if (settingsId == null) {
+            log.warn("查询营业时间列表失败:设置ID不能为空");
+            return Collections.emptyList();
+        }
+        
+        LambdaQueryWrapper<StoreBookingBusinessHours> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getSort);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getId);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public List<StoreBookingBusinessHours> getListBySettingsIdAndType(Integer settingsId, Integer businessType) {
+        log.info("StoreBookingBusinessHoursServiceImpl.getListBySettingsIdAndType?settingsId={}, businessType={}", settingsId, businessType);
+        
+        if (settingsId == null) {
+            log.warn("查询营业时间列表失败:设置ID不能为空");
+            return Collections.emptyList();
+        }
+        
+        LambdaQueryWrapper<StoreBookingBusinessHours> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+        if (businessType != null) {
+            wrapper.eq(StoreBookingBusinessHours::getBusinessType, businessType);
+        }
+        wrapper.orderByAsc(StoreBookingBusinessHours::getSort);
+        wrapper.orderByAsc(StoreBookingBusinessHours::getId);
+        // @TableLogic 会自动过滤已删除的记录(delete_flag=0)
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public boolean saveOrUpdateBusinessHours(StoreBookingBusinessHours businessHours) {
+        log.info("StoreBookingBusinessHoursServiceImpl.saveOrUpdateBusinessHours?businessHours={}", businessHours);
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 参数验证
+        if (businessHours.getSettingsId() == null) {
+            log.warn("保存营业时间失败:设置ID不能为空");
+            throw new RuntimeException("设置ID不能为空");
+        }
+        if (businessHours.getBusinessType() == null) {
+            log.warn("保存营业时间失败:营业类型不能为空");
+            throw new RuntimeException("营业类型不能为空");
+        }
+        if (businessHours.getBookingTimeType() == null) {
+            log.warn("保存营业时间失败:预订时间类型不能为空");
+            throw new RuntimeException("预订时间类型不能为空");
+        }
+        
+        // 如果是特殊营业(节假日),验证节假日类型
+        if (businessHours.getBusinessType() != null && businessHours.getBusinessType() == 1) {
+            if (!StringUtils.hasText(businessHours.getHolidayType())) {
+                log.warn("保存营业时间失败:特殊营业时必须填写节假日类型");
+                throw new RuntimeException("特殊营业时必须填写节假日类型");
+            }
+        }
+        
+        // 如果选择非全天,必须填写开始时间和结束时间
+        if (businessHours.getBookingTimeType() != null && businessHours.getBookingTimeType() == 0) {
+            if (!StringUtils.hasText(businessHours.getStartTime())) {
+                log.warn("保存营业时间失败:非全天时必须填写开始时间");
+                throw new RuntimeException("非全天时必须填写开始时间");
+            }
+            if (!StringUtils.hasText(businessHours.getEndTime())) {
+                log.warn("保存营业时间失败:非全天时必须填写结束时间");
+                throw new RuntimeException("非全天时必须填写结束时间");
+            }
+            // 验证时间格式
+            if (!TIME_PATTERN.matcher(businessHours.getStartTime()).matches()) {
+                log.warn("保存营业时间失败:开始时间格式不正确,应为HH:mm格式");
+                throw new RuntimeException("开始时间格式不正确,应为HH:mm格式");
+            }
+            if (!TIME_PATTERN.matcher(businessHours.getEndTime()).matches()) {
+                log.warn("保存营业时间失败:结束时间格式不正确,应为HH:mm格式");
+                throw new RuntimeException("结束时间格式不正确,应为HH:mm格式");
+            }
+            // 验证开始时间必须小于结束时间(支持跨天,如22:00到次日02:00)
+            // 这里只做基本格式验证,跨天逻辑由业务层处理
+        } else {
+            // 如果是全天,清空开始时间和结束时间
+            businessHours.setStartTime(null);
+            businessHours.setEndTime(null);
+        }
+        
+        // 设置排序,如果为空则默认为0
+        if (businessHours.getSort() == null) {
+            businessHours.setSort(0);
+        }
+        
+        if (businessHours.getId() != null) {
+            // 更新
+            LambdaUpdateWrapper<StoreBookingBusinessHours> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreBookingBusinessHours::getId, businessHours.getId());
+            
+            updateWrapper.set(StoreBookingBusinessHours::getBusinessType, businessHours.getBusinessType());
+            updateWrapper.set(StoreBookingBusinessHours::getHolidayType, businessHours.getHolidayType());
+            updateWrapper.set(StoreBookingBusinessHours::getHolidayDate, businessHours.getHolidayDate());
+            updateWrapper.set(StoreBookingBusinessHours::getBookingTimeType, businessHours.getBookingTimeType());
+            updateWrapper.set(StoreBookingBusinessHours::getStartTime, businessHours.getStartTime());
+            updateWrapper.set(StoreBookingBusinessHours::getEndTime, businessHours.getEndTime());
+            updateWrapper.set(StoreBookingBusinessHours::getSort, businessHours.getSort());
+            
+            if (userId != null) {
+                updateWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+            }
+            
+            return this.update(updateWrapper);
+        } else {
+            // 新增
+            businessHours.setCreatedUserId(userId);
+            return this.save(businessHours);
+        }
+    }
+
+    @Override
+    public boolean batchSaveBusinessHours(Integer settingsId, List<StoreBookingBusinessHours> businessHoursList) {
+        log.info("StoreBookingBusinessHoursServiceImpl.batchSaveBusinessHours?settingsId={}, size={}", settingsId, businessHoursList != null ? businessHoursList.size() : 0);
+        
+        if (settingsId == null) {
+            log.warn("批量保存营业时间失败:设置ID不能为空");
+            throw new RuntimeException("设置ID不能为空");
+        }
+        if (businessHoursList == null || businessHoursList.isEmpty()) {
+            log.warn("批量保存营业时间失败:营业时间列表不能为空");
+            throw new RuntimeException("营业时间列表不能为空");
+        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 先删除该设置下的所有营业时间(逻辑删除)
+        LambdaUpdateWrapper<StoreBookingBusinessHours> deleteWrapper = new LambdaUpdateWrapper<>();
+        deleteWrapper.eq(StoreBookingBusinessHours::getSettingsId, settingsId);
+        deleteWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+        if (userId != null) {
+            deleteWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+        }
+        this.update(deleteWrapper);
+        
+        // 批量保存新的营业时间
+        for (StoreBookingBusinessHours businessHours : businessHoursList) {
+            businessHours.setSettingsId(settingsId);
+            businessHours.setId(null); // 确保是新增
+            businessHours.setCreatedUserId(userId);
+            // 验证并保存
+            saveOrUpdateBusinessHours(businessHours);
+        }
+        
+        return true;
+    }
+
+    @Override
+    public boolean deleteBusinessHours(Integer id) {
+        log.info("StoreBookingBusinessHoursServiceImpl.deleteBusinessHours?id={}", id);
+        
+        if (id == null) {
+            log.warn("删除营业时间失败:营业时间ID不能为空");
+            throw new RuntimeException("营业时间ID不能为空");
+        }
+        
+        // 从JWT获取当前登录用户ID
+        Integer userId = getCurrentUserId();
+        
+        // 逻辑删除
+        LambdaUpdateWrapper<StoreBookingBusinessHours> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreBookingBusinessHours::getId, id);
+        updateWrapper.set(StoreBookingBusinessHours::getDeleteFlag, 1);
+        if (userId != null) {
+            updateWrapper.set(StoreBookingBusinessHours::getUpdatedUserId, userId);
+        }
+        
+        return this.update(updateWrapper);
+    }
+
+    /**
+     * 从JWT获取当前登录用户ID
+     *
+     * @return 用户ID,如果未登录返回null
+     */
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+}

+ 13 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingCategoryServiceImpl.java

@@ -10,8 +10,10 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.store.StoreBookingCategory;
+import shop.alien.entity.store.StoreBookingTable;
 import shop.alien.mapper.StoreBookingCategoryMapper;
 import shop.alien.store.service.StoreBookingCategoryService;
+import shop.alien.store.service.StoreBookingTableService;
 import shop.alien.util.common.JwtUtil;
 
 import java.util.List;
@@ -28,6 +30,8 @@ import java.util.List;
 @RequiredArgsConstructor
 public class StoreBookingCategoryServiceImpl extends ServiceImpl<StoreBookingCategoryMapper, StoreBookingCategory> implements StoreBookingCategoryService {
 
+    private final StoreBookingTableService storeBookingTableService;
+
     @Override
     public List<StoreBookingCategory> getCategoryList(Integer storeId) {
         log.info("StoreBookingCategoryServiceImpl.getCategoryList?storeId={}", storeId);
@@ -203,6 +207,15 @@ public class StoreBookingCategoryServiceImpl extends ServiceImpl<StoreBookingCat
             throw new RuntimeException("分类不存在");
         }
         
+        // 校验:检查当前分类下是否有桌号,如果有则不允许删除
+        LambdaQueryWrapper<StoreBookingTable> tableWrapper = new LambdaQueryWrapper<>();
+        tableWrapper.eq(StoreBookingTable::getCategoryId, id);
+        long tableCount = storeBookingTableService.count(tableWrapper);
+        if (tableCount > 0) {
+            log.warn("删除预订服务分类失败:当前分类下存在桌号,需要先删除桌号才能删除该分类,categoryId={}, tableCount={}", id, tableCount);
+            throw new RuntimeException("当前分类下存在桌号,需要先删除桌号才能删除该分类");
+        }
+        
         // 逻辑删除
         return this.removeById(id);
     }

+ 223 - 27
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java

@@ -6,14 +6,22 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreBookingBusinessHours;
 import shop.alien.entity.store.StoreBookingSettings;
+import shop.alien.entity.store.dto.StoreBookingBusinessHoursDTO;
+import shop.alien.entity.store.dto.StoreBookingSettingsDTO;
 import shop.alien.mapper.StoreBookingSettingsMapper;
+import shop.alien.store.service.StoreBookingBusinessHoursService;
 import shop.alien.store.service.StoreBookingSettingsService;
 import shop.alien.util.common.JwtUtil;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
 import java.util.regex.Pattern;
 
 /**
@@ -28,6 +36,8 @@ import java.util.regex.Pattern;
 @RequiredArgsConstructor
 public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSettingsMapper, StoreBookingSettings> implements StoreBookingSettingsService {
 
+    private final StoreBookingBusinessHoursService storeBookingBusinessHoursService;
+
     // 时间格式正则:HH:mm
     private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3]):[0-5][0-9]$");
 
@@ -80,36 +90,60 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
             throw new RuntimeException("单时段最大容纳人数必须大于0");
         }
         
-        // 如果选择非全天,必须填写开始时间和结束时间
-        if (settings.getBookingTimeType() != null && settings.getBookingTimeType() == 0) {
-            if (!StringUtils.hasText(settings.getBookingStartTime())) {
-                log.warn("保存预订服务信息设置失败:非全天时必须填写开始时间");
-                throw new RuntimeException("非全天时必须填写开始时间");
-            }
-            if (!StringUtils.hasText(settings.getBookingEndTime())) {
-                log.warn("保存预订服务信息设置失败:非全天时必须填写结束时间");
-                throw new RuntimeException("非全天时必须填写结束时间");
-            }
-            // 验证时间格式
-            if (!TIME_PATTERN.matcher(settings.getBookingStartTime()).matches()) {
-                log.warn("保存预订服务信息设置失败:开始时间格式不正确,应为HH:mm格式");
-                throw new RuntimeException("开始时间格式不正确,应为HH:mm格式");
+        // 验证预订相关字段
+        // 如果选择付费(reservation=1),必须填写预订金额
+        if (StringUtils.hasText(settings.getReservation()) && "1".equals(settings.getReservation())) {
+            if (settings.getReservationMoney() == null || settings.getReservationMoney() < 0) {
+                log.warn("保存预订服务信息设置失败:选择付费时必须填写预订金额,且金额不能小于0");
+                throw new RuntimeException("选择付费时必须填写预订金额,且金额不能小于0");
             }
-            if (!TIME_PATTERN.matcher(settings.getBookingEndTime()).matches()) {
-                log.warn("保存预订服务信息设置失败:结束时间格式不正确,应为HH:mm格式");
-                throw new RuntimeException("结束时间格式不正确,应为HH:mm格式");
-            }
-            // 验证开始时间必须小于结束时间
-            if (settings.getBookingStartTime().compareTo(settings.getBookingEndTime()) >= 0) {
-                log.warn("保存预订服务信息设置失败:开始时间必须小于结束时间");
-                throw new RuntimeException("开始时间必须小于结束时间");
-            }
-        } else {
-            // 如果是全天,清空开始时间和结束时间
-            settings.setBookingStartTime(null);
-            settings.setBookingEndTime(null);
+        } else if (StringUtils.hasText(settings.getReservation()) && "0".equals(settings.getReservation())) {
+            // 如果选择免费,清空预订金额
+            settings.setReservationMoney(null);
+        }
+        
+        // 验证取消预订退费时长设置
+        if (settings.getOffUnsubscribeHours() == null || settings.getOffUnsubscribeHours() < 0) {
+            log.warn("保存预订服务信息设置失败:取消预订退费时长设置必须大于等于0");
+            throw new RuntimeException("取消预订退费时长设置必须大于等于0");
+        }
+        
+        // 验证营业时间结束前不可预订时间
+        if (settings.getBookingNotAvailableTime() == null || settings.getBookingNotAvailableTime() < 0) {
+            log.warn("保存预订服务信息设置失败:营业时间结束前不可预订时间必须大于等于0");
+            throw new RuntimeException("营业时间结束前不可预订时间必须大于等于0");
         }
         
+        // 如果选择非全天,必须填写开始时间和结束时间
+//        if (settings.getBookingTimeType() != null && settings.getBookingTimeType() == 0) {
+//            if (!StringUtils.hasText(settings.getBookingStartTime())) {
+//                log.warn("保存预订服务信息设置失败:非全天时必须填写开始时间");
+//                throw new RuntimeException("非全天时必须填写开始时间");
+//            }
+//            if (!StringUtils.hasText(settings.getBookingEndTime())) {
+//                log.warn("保存预订服务信息设置失败:非全天时必须填写结束时间");
+//                throw new RuntimeException("非全天时必须填写结束时间");
+//            }
+//            // 验证时间格式
+//            if (!TIME_PATTERN.matcher(settings.getBookingStartTime()).matches()) {
+//                log.warn("保存预订服务信息设置失败:开始时间格式不正确,应为HH:mm格式");
+//                throw new RuntimeException("开始时间格式不正确,应为HH:mm格式");
+//            }
+//            if (!TIME_PATTERN.matcher(settings.getBookingEndTime()).matches()) {
+//                log.warn("保存预订服务信息设置失败:结束时间格式不正确,应为HH:mm格式");
+//                throw new RuntimeException("结束时间格式不正确,应为HH:mm格式");
+//            }
+//            // 验证开始时间必须小于结束时间
+//            if (settings.getBookingStartTime().compareTo(settings.getBookingEndTime()) >= 0) {
+//                log.warn("保存预订服务信息设置失败:开始时间必须小于结束时间");
+//                throw new RuntimeException("开始时间必须小于结束时间");
+//            }
+//        } else {
+//            // 如果是全天,清空开始时间和结束时间
+//            settings.setBookingStartTime(null);
+//            settings.setBookingEndTime(null);
+//        }
+        
         // 查询是否已存在该门店的设置
         StoreBookingSettings existingSettings = getByStoreId(settings.getStoreId());
         
@@ -126,6 +160,10 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
             updateWrapper.set(StoreBookingSettings::getBookingStartTime, settings.getBookingStartTime());
             updateWrapper.set(StoreBookingSettings::getBookingEndTime, settings.getBookingEndTime());
             updateWrapper.set(StoreBookingSettings::getMaxCapacityPerSlot, settings.getMaxCapacityPerSlot());
+            updateWrapper.set(StoreBookingSettings::getReservation, settings.getReservation());
+            updateWrapper.set(StoreBookingSettings::getReservationMoney, settings.getReservationMoney());
+            updateWrapper.set(StoreBookingSettings::getOffUnsubscribeHours, settings.getOffUnsubscribeHours());
+            updateWrapper.set(StoreBookingSettings::getBookingNotAvailableTime, settings.getBookingNotAvailableTime());
             
             if (userId != null) {
                 updateWrapper.set(StoreBookingSettings::getUpdatedUserId, userId);
@@ -139,6 +177,164 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
         }
     }
 
+    @Override
+    public boolean saveSettingsWithBusinessHours(StoreBookingSettingsDTO dto) {
+        log.info("StoreBookingSettingsServiceImpl.saveSettingsWithBusinessHours?dto={}", dto);
+        
+        // 1. 先保存或更新设置信息
+        StoreBookingSettings settings = new StoreBookingSettings();
+        settings.setId(dto.getId());
+        settings.setStoreId(dto.getStoreId());
+        settings.setRetainPositionFlag(dto.getRetainPositionFlag());
+        settings.setRetentionDuration(dto.getRetentionDuration());
+        settings.setBookingDateDisplayDays(dto.getBookingDateDisplayDays());
+        settings.setBookingTimeType(dto.getBookingTimeType());
+        settings.setBookingStartTime(dto.getBookingStartTime());
+        settings.setBookingEndTime(dto.getBookingEndTime());
+        settings.setMaxCapacityPerSlot(dto.getMaxCapacityPerSlot());
+        settings.setReservation(dto.getReservation());
+        settings.setReservationMoney(dto.getReservationMoney());
+        settings.setOffUnsubscribeHours(dto.getOffUnsubscribeHours());
+        settings.setBookingNotAvailableTime(dto.getBookingNotAvailableTime());
+        
+        boolean settingsResult = saveOrUpdateSettings(settings);
+        if (!settingsResult) {
+            log.error("保存预订服务信息设置失败");
+            throw new RuntimeException("保存预订服务信息设置失败");
+        }
+        
+        // 2. 获取保存后的设置ID(如果是新增,需要获取新生成的ID)
+        Integer settingsId = settings.getId();
+        if (settingsId == null) {
+            // 如果是新增,重新查询获取ID
+            StoreBookingSettings savedSettings = getByStoreId(dto.getStoreId());
+            if (savedSettings == null) {
+                log.error("保存设置后无法获取设置ID");
+                throw new RuntimeException("保存设置后无法获取设置ID");
+            }
+            settingsId = savedSettings.getId();
+        }
+        
+        // 3. 准备营业时间列表
+        List<StoreBookingBusinessHours> businessHoursList = new ArrayList<>();
+        
+        // 3.1 处理正常营业时间(business_type=0)
+        if (dto.getNormalBusinessHours() != null) {
+            StoreBookingBusinessHours normalHours = convertToBusinessHours(dto.getNormalBusinessHours(), settingsId, 0);
+            businessHoursList.add(normalHours);
+        }
+        
+        // 3.2 处理特殊营业时间列表(business_type=1)
+        if (dto.getSpecialBusinessHoursList() != null && !dto.getSpecialBusinessHoursList().isEmpty()) {
+            int sort = 1;
+            for (StoreBookingBusinessHoursDTO specialDto : dto.getSpecialBusinessHoursList()) {
+                StoreBookingBusinessHours specialHours = convertToBusinessHours(specialDto, settingsId, 1);
+                specialHours.setSort(sort++);
+                businessHoursList.add(specialHours);
+            }
+        }
+        
+        // 4. 批量保存营业时间(如果列表不为空)
+        if (!businessHoursList.isEmpty()) {
+            boolean businessHoursResult = storeBookingBusinessHoursService.batchSaveBusinessHours(settingsId, businessHoursList);
+            if (!businessHoursResult) {
+                log.error("保存营业时间失败");
+                throw new RuntimeException("保存营业时间失败");
+            }
+        }
+        
+        return true;
+    }
+
+    @Override
+    public StoreBookingSettingsDTO getSettingsWithBusinessHoursByStoreId(Integer storeId) {
+        log.info("StoreBookingSettingsServiceImpl.getSettingsWithBusinessHoursByStoreId?storeId={}", storeId);
+        
+        if (storeId == null) {
+            log.warn("查询预订服务信息设置失败:门店ID不能为空");
+            return null;
+        }
+        
+        // 1. 查询设置信息
+        StoreBookingSettings settings = getByStoreId(storeId);
+        if (settings == null) {
+            return null;
+        }
+        
+        // 2. 转换为DTO
+        StoreBookingSettingsDTO dto = new StoreBookingSettingsDTO();
+        BeanUtils.copyProperties(settings, dto);
+        
+        // 3. 查询营业时间列表
+        List<StoreBookingBusinessHours> businessHoursList = storeBookingBusinessHoursService.getListBySettingsId(settings.getId());
+        
+        // 4. 分离正常营业时间和特殊营业时间
+        StoreBookingBusinessHours normalHours = businessHoursList.stream()
+                .filter(h -> h.getBusinessType() != null && h.getBusinessType() == 0)
+                .findFirst()
+                .orElse(null);
+        
+        List<StoreBookingBusinessHours> specialHoursList = businessHoursList.stream()
+                .filter(h -> h.getBusinessType() != null && h.getBusinessType() == 1)
+                .collect(Collectors.toList());
+        
+        // 5. 转换为DTO
+        if (normalHours != null) {
+            dto.setNormalBusinessHours(convertToBusinessHoursDTO(normalHours));
+        }
+        
+        if (!specialHoursList.isEmpty()) {
+            List<StoreBookingBusinessHoursDTO> specialHoursDTOList = specialHoursList.stream()
+                    .map(this::convertToBusinessHoursDTO)
+                    .collect(Collectors.toList());
+            dto.setSpecialBusinessHoursList(specialHoursDTOList);
+        }
+        
+        return dto;
+    }
+
+    /**
+     * 将DTO转换为营业时间实体
+     *
+     * @param dto DTO对象
+     * @param settingsId 设置ID
+     * @param businessType 营业类型(0:正常营业, 1:特殊营业/节假日)
+     * @return StoreBookingBusinessHours
+     */
+    private StoreBookingBusinessHours convertToBusinessHours(StoreBookingBusinessHoursDTO dto, Integer settingsId, Integer businessType) {
+        StoreBookingBusinessHours businessHours = new StoreBookingBusinessHours();
+        businessHours.setId(dto.getId());
+        businessHours.setSettingsId(settingsId);
+        businessHours.setBusinessType(businessType);
+        businessHours.setHolidayType(dto.getHolidayType());
+        businessHours.setHolidayDate(dto.getHolidayDate());
+        businessHours.setBookingTimeType(dto.getBookingTimeType());
+        businessHours.setStartTime(dto.getStartTime());
+        businessHours.setEndTime(dto.getEndTime());
+        businessHours.setSort(dto.getSort() != null ? dto.getSort() : 0);
+        return businessHours;
+    }
+
+    /**
+     * 将营业时间实体转换为DTO
+     *
+     * @param businessHours 营业时间实体
+     * @return StoreBookingBusinessHoursDTO
+     */
+    private StoreBookingBusinessHoursDTO convertToBusinessHoursDTO(StoreBookingBusinessHours businessHours) {
+        StoreBookingBusinessHoursDTO dto = new StoreBookingBusinessHoursDTO();
+        dto.setId(businessHours.getId());
+        dto.setSettingsId(businessHours.getSettingsId());
+        dto.setBusinessType(businessHours.getBusinessType());
+        dto.setHolidayType(businessHours.getHolidayType());
+        dto.setHolidayDate(businessHours.getHolidayDate());
+        dto.setBookingTimeType(businessHours.getBookingTimeType());
+        dto.setStartTime(businessHours.getStartTime());
+        dto.setEndTime(businessHours.getEndTime());
+        dto.setSort(businessHours.getSort());
+        return dto;
+    }
+
     /**
      * 从JWT获取当前登录用户ID
      *

+ 69 - 3
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingTableServiceImpl.java

@@ -14,6 +14,7 @@ import shop.alien.mapper.StoreBookingTableMapper;
 import shop.alien.store.service.StoreBookingTableService;
 import shop.alien.util.common.JwtUtil;
 
+import java.util.Comparator;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -41,10 +42,75 @@ public class StoreBookingTableServiceImpl extends ServiceImpl<StoreBookingTableM
             wrapper.eq(StoreBookingTable::getCategoryId, categoryId);
         }
         
-        // 按创建时间倒序排列
-        wrapper.orderByDesc(StoreBookingTable::getCreatedTime);
+        // 查询所有数据,然后在Java中进行排序
+        List<StoreBookingTable> list = this.list(wrapper);
         
-        return this.list(wrapper);
+        // 自定义排序:根据类别、字母(A-Z)、数字(由小到大)顺序排列
+        list.sort(Comparator
+                .comparing(StoreBookingTable::getCategoryId, Comparator.nullsLast(Integer::compareTo)) // 先按类别排序
+                .thenComparing(table -> parseTableNumberForSort(table.getTableNumber()))); // 再按桌号排序
+        
+        return list;
+    }
+    
+    /**
+     * 解析桌号用于排序
+     * 规则:字母(A-Z)优先,然后数字(由小到大)
+     * 排序逻辑:
+     * 1. 纯数字桌号(如:1, 2, 10)按数字从小到大排序
+     * 2. 字母桌号(如:A1, B2)按字母A-Z排序,然后按数字从小到大排序
+     * 3. 字母桌号优先于纯数字桌号
+     * 
+     * @param tableNumber 桌号
+     * @return 排序键
+     */
+    private String parseTableNumberForSort(String tableNumber) {
+        if (tableNumber == null || tableNumber.isEmpty()) {
+            return "ZZZZZZZZZZ9999999999"; // 空值排在最后
+        }
+        
+        // 去除空格并转为大写,便于比较
+        String normalized = tableNumber.trim().toUpperCase();
+        
+        // 分离字母和数字部分
+        StringBuilder letters = new StringBuilder();
+        StringBuilder numbers = new StringBuilder();
+        
+        for (char c : normalized.toCharArray()) {
+            if (Character.isLetter(c)) {
+                letters.append(c);
+            } else if (Character.isDigit(c)) {
+                numbers.append(c);
+            }
+        }
+        
+        String letterPart = letters.toString();
+        String numberPart = numbers.toString();
+        
+        // 数字部分处理:转换为整数再格式化,确保数字排序正确(如:1 < 2 < 10)
+        if (numberPart.isEmpty()) {
+            numberPart = "0000000000";
+        } else {
+            try {
+                int num = Integer.parseInt(numberPart);
+                numberPart = String.format("%010d", num); // 补零到10位,确保数字排序正确
+            } catch (NumberFormatException e) {
+                // 如果数字太大超出int范围,保持原样并补零
+                numberPart = String.format("%-10s", numberPart).replace(' ', '0');
+            }
+        }
+        
+        // 字母部分处理
+        if (letterPart.isEmpty()) {
+            // 纯数字桌号:使用特殊前缀确保排在字母桌号后面
+            // 但根据用户需求,可能需要纯数字排在前面,这里先按字母优先的逻辑
+            letterPart = "ZZZZZZZZZZ"; // 纯数字排在字母后面
+        } else {
+            // 字母桌号:补空格到固定长度,确保字母排序正确
+            letterPart = String.format("%-10s", letterPart);
+        }
+        
+        return letterPart + numberPart;
     }
 
     @Override