Browse Source

Merge remote-tracking branch 'origin/dev' into dev

jinshihui 5 giờ trước cách đây
mục cha
commit
b3dbbb60b7

+ 12 - 4
nightFragrance-massage/src/main/java/com/ylx/order/controller/OrderController.java

@@ -2,10 +2,7 @@ package com.ylx.order.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.common.core.domain.R;
-import com.ylx.order.domain.dto.OrderCancleDTO;
-import com.ylx.order.domain.dto.OrderDateQueryDTO;
-import com.ylx.order.domain.dto.OrderDeleteDTO;
-import com.ylx.order.domain.dto.OrderSubmitDTO;
+import com.ylx.order.domain.dto.*;
 import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.domain.vo.OrderDetailVO;
 import com.ylx.order.service.TOrderService;
@@ -60,6 +57,8 @@ public class OrderController {
             @PathVariable @ApiParam(value = "订单ID", required = true, example = "1") Long orderId) {
         return R.ok(orderService.getOrderDetailById(orderId));
     }
+
+    @PreAuthorize("@customerAuth.isCustomer()")
     @ApiOperation("用户取消订单")
     @PostMapping("/cancel")
     public R<?> cancelOrder(@RequestBody @Validated OrderCancleDTO dto) {
@@ -67,4 +66,13 @@ public class OrderController {
         int result = orderService.cancelOrder(dto);
         return result > 0 ? R.ok("订单已取消") : R.fail("订单取消失败");
     }
+
+    @PreAuthorize("@customerAuth.isCustomer()")
+    @ApiOperation("客户端是否可以预约当前时段")
+    @PostMapping("/booking/check")
+    public R<Boolean> bookingCheck(@RequestBody @Validated BookingCheckDTO dto) {
+        Boolean result = orderService.bookingCheck(dto);
+        return R.ok(result);
+    }
+
 }

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/TOrder.java

@@ -186,4 +186,7 @@ public class TOrder extends BaseEntity {
 
     @ApiModelProperty("优惠券id")
     private String couponId;
+
+    @ApiModelProperty("预约结束时间")
+    private LocalDateTime appointmentEndTime;
 }

+ 34 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/BookingCheckDTO.java

@@ -0,0 +1,34 @@
+package com.ylx.order.domain.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+import java.time.LocalTime;
+
+@Data
+@ApiModel("客户端预约校验DTO")
+public class BookingCheckDTO {
+
+    @NotNull(message = "商户ID不能为空")
+    @ApiModelProperty("商户ID")
+    private Long merchantId;
+
+    @NotNull(message = "项目ID不能为空")
+    @ApiModelProperty("项目ID")
+    private Long projectId;
+
+    @NotNull(message = "预约日期不能为空")
+    @ApiModelProperty(value = "预约日期", example = "2026-06-12")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate appointmentDate;
+
+    @NotNull(message = "预约开始时间不能为空")
+    @ApiModelProperty(value = "预约开始时间", example = "14:30")
+    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
+    private LocalTime startTime;
+
+}

+ 8 - 1
nightFragrance-massage/src/main/java/com/ylx/order/mapper/TOrderMapper.java

@@ -3,11 +3,12 @@ package com.ylx.order.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ylx.massage.domain.vo.HomeBlock;
+import com.ylx.order.domain.TOrder;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
-import com.ylx.order.domain.TOrder;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 
@@ -39,4 +40,10 @@ public interface TOrderMapper extends BaseMapper<TOrder> {
 
     List<TOrder> getAll(@Param("param") TOrder param);
 
+    List<TOrder> selectValidOrdersByMerchantAndDate(
+            @Param("merchantId") Long merchantId,
+            @Param("projectId") Long projectId,
+            @Param("dayStart") LocalDateTime dayStart,
+            @Param("dayEnd") LocalDateTime dayEnd
+    );
 }

+ 3 - 4
nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java

@@ -9,10 +9,7 @@ import com.ylx.massage.domain.TWxUser;
 import com.ylx.massage.domain.vo.HomeBlock;
 import com.ylx.massage.domain.vo.OrderVerificationVo;
 import com.ylx.massage.domain.vo.TechnicianAvailabilityVo;
-import com.ylx.order.domain.dto.OrderCancleDTO;
-import com.ylx.order.domain.dto.OrderDateQueryDTO;
-import com.ylx.order.domain.dto.OrderSubmitDTO;
-import com.ylx.order.domain.dto.OrderUpdateStatusDTO;
+import com.ylx.order.domain.dto.*;
 import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.domain.vo.OrderDetailVO;
 
@@ -192,4 +189,6 @@ public interface TOrderService extends IService<TOrder> {
      * @return 取消结果
      */
     int cancelOrder(OrderCancleDTO dto);
+
+    Boolean bookingCheck(BookingCheckDTO dto);
 }

+ 97 - 7
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -9,9 +9,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.ylx.common.core.domain.R;
+import com.ylx.common.core.domain.entity.SysDictData;
 import com.ylx.common.core.domain.model.WxLoginUser;
 import com.ylx.common.exception.ServiceException;
 import com.ylx.common.utils.DateUtils;
+import com.ylx.common.utils.DictUtils;
 import com.ylx.common.utils.SecurityUtils;
 import com.ylx.common.utils.StringUtils;
 import com.ylx.common.weixinPay.enums.WxPayTypeEnum;
@@ -33,10 +35,7 @@ import com.ylx.massage.utils.OrderNumberGenerator;
 import com.ylx.order.domain.AfterSalesService;
 import com.ylx.order.domain.OrderStatusFlow;
 import com.ylx.order.domain.TOrder;
-import com.ylx.order.domain.dto.OrderCancleDTO;
-import com.ylx.order.domain.dto.OrderDateQueryDTO;
-import com.ylx.order.domain.dto.OrderSubmitDTO;
-import com.ylx.order.domain.dto.OrderUpdateStatusDTO;
+import com.ylx.order.domain.dto.*;
 import com.ylx.order.domain.vo.OrderDateQueryVo;
 import com.ylx.order.domain.vo.OrderDetailVO;
 import com.ylx.order.domain.vo.OrderStatusFlowVO;
@@ -81,7 +80,9 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("M月d日");
     private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
 
-    /** 仅允许取消的状态:待付款(0) */
+    /**
+     * 仅允许取消的状态:待付款(0)
+     */
     private static final List<Integer> ALLOWED_CANCEL_STATUS = Collections.singletonList(0);
     @Resource
     private ProjectService projectService;
@@ -110,6 +111,8 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     private MaProjectMapper maProjectMapper;
     @Resource
     private IAfterSalesServiceService afterSalesServiceService;
+    private static final String DICT_TYPE = "unit_type";
+    private static final int BUFFER_MINUTES = 30; // 30分钟缓冲期
 
     @Override
     public TOrder addOrder(TOrder order) {
@@ -416,6 +419,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     /**
      *
      * 更新订单状态(修改订单表状态必须使用此接口)
+     *
      * @param dto
      */
     @Override
@@ -505,7 +509,10 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         order.setProjectCover(project.getCover());
         order.setHighlight(project.getHighlight());
         order.setAppointmentStartTime(dto.getAppointmentStartTime());
-        order.setProjectDuration(project.getStandardDuration());
+
+        // 根据项目的unitType字段将小时转换为分钟
+        Integer projectDuration = this.convertToMinutes(project.getStandardDuration(), project.getUnitType());
+        order.setProjectDuration(projectDuration);
         order.setMerchantId(dto.getMerchantId());
         order.setMerchantType(maTechnician.getTechType());
         order.setMerchantNickName(maTechnician.getTeNickName());
@@ -519,7 +526,6 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         order.setFinalAmount(finalAmount);
         order.setCouponId(dto.getCouponId());
         order.setCreateTime(DateUtils.getNowDate());
-        order.setAppointmentStartTime(dto.getAppointmentStartTime());
         order.setStatus(0);
         order.setExecStatus(0);
         order.setDispatchedStatus(0);
@@ -625,6 +631,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         // 3. 组装VO
         return buildOrderDetailVO(order, flowList);
     }
+
     /**
      * 将订单实体和流转记录转换为详情VO
      */
@@ -718,8 +725,10 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
 
         return vo;
     }
+
     /**
      * 格式化预约时间范围
+     *
      * @param start 预约开始时间
      * @param end   预约结束时间
      * @return 示例:5月12日 16:00-18:00
@@ -737,6 +746,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         String endTime = (end != null) ? end.format(timeFormatter) : "";
         return dateStr + " " + startTime + "-" + endTime;
     }
+
     /**
      * 构建联系信息(姓名 + 脱敏手机号)
      * 示例:王先生,188****5555
@@ -814,6 +824,48 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         return rows;
     }
 
+    @Override
+    public Boolean bookingCheck(BookingCheckDTO dto) {
+
+        // 1. 根据 projectId 获取项目时长
+        Project project = projectService.getById(dto.getProjectId());
+        if (ObjectUtil.isNull(project)) {
+            throw new ServiceException("项目不存在");
+        }
+        Integer duration = convertToMinutes(project.getStandardDuration(), project.getUnitType());
+
+        // 2. 组装目标时间段
+        LocalDateTime newStart = LocalDateTime.of(dto.getAppointmentDate(), dto.getStartTime());
+        LocalDateTime actualEnd = newStart.plusMinutes(duration); // 实际服务结束时间
+        LocalDateTime newEndWithBuffer = actualEnd.plusMinutes(BUFFER_MINUTES); // 含30分钟缓冲的结束时间
+
+        // 3. 查询该商户当天的【有效占用】订单
+        LocalDateTime dayStart = dto.getAppointmentDate().atStartOfDay();
+        LocalDateTime dayEnd = dto.getAppointmentDate().atTime(LocalTime.MAX);
+
+        List<TOrder> validOrders = this.baseMapper.selectValidOrdersByMerchantAndDate(
+                dto.getMerchantId(),
+                dto.getProjectId(),
+                dayStart,
+                dayEnd
+        );
+
+        // 4. 内存中遍历判断重叠
+        for (TOrder order : validOrders) {
+            LocalDateTime existStart = order.getAppointmentStartTime();
+            LocalDateTime existEnd = order.getAppointmentEndTime();
+
+            // 核心重叠算法:
+            // 新订单开始时间 < 已有订单结束时间  且  新订单结束时间(含30min缓冲) > 已有订单开始时间
+            if (newStart.isBefore(existEnd) && newEndWithBuffer.isAfter(existStart)) {
+                return false;
+            }
+        }
+
+        // 5. 没有任何冲突,可以预约
+        return true;
+    }
+
     private void fillCurrentAfterSaleInfo(IAfterSaleDisplay vo, Long orderId) {
         LambdaQueryWrapper<AfterSalesService> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AfterSalesService::getOrderId, orderId)
@@ -835,4 +887,42 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         }
 
     }
+
+    /**
+     * 根据单位类型将时长转换为分钟
+     *
+     * @param duration 原始时长
+     * @param unitType 单位类型 (对应字典表: 1-分钟, 2-小时, 3-小时/次)
+     * @return 转换后的分钟数
+     */
+    private Integer convertToMinutes(Integer duration, Integer unitType) {
+        // 1. 防御性校验:避免自动拆箱导致的 NPE
+        if (duration == null || duration <= 0 || unitType == null) {
+            return duration;
+        }
+
+        // 2. 从字典缓存中获取数据(若依的 DictUtils 读取成本极低,无需自己做 DCL 锁)
+        List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_TYPE);
+        if (CollUtil.isEmpty(dictList)) {
+            return duration;
+        }
+
+        // 3. 使用局部变量构建映射,彻底杜绝多线程并发修改 HashMap 的风险
+        Map<Integer, Integer> localCoeffMap = new HashMap<>(dictList.size());
+        for (SysDictData data : dictList) {
+            try {
+                int type = Integer.parseInt(data.getDictValue());
+                // 根据业务规则设置倍率
+                int ratio = (type == 1) ? 1 : 60;
+                localCoeffMap.put(type, ratio);
+            } catch (NumberFormatException e) {
+                log.warn("字典数据格式错误,跳过该条记录: {}", data);
+            }
+        }
+
+        // 4. 获取倍率并计算,默认倍率为 1(即不转换)
+        int factor = localCoeffMap.getOrDefault(unitType, 1);
+        return duration * factor;
+    }
+
 }

+ 15 - 0
nightFragrance-massage/src/main/resources/mapper/order/TOrderMapper.xml

@@ -291,4 +291,19 @@
     <select id="callAutoAccount" statementType="CALLABLE">
         {call updateUserBalance(#{hCount, mode=IN, jdbcType=INTEGER}, #{percent, mode=IN, jdbcType=DECIMAL})}
     </select>
+
+    <select id="selectValidOrdersByMerchantAndDate" resultType="com.ylx.order.domain.TOrder">
+        SELECT
+            appointment_start_time,
+            appointment_end_time
+        FROM t_order
+        WHERE merchant_id = #{merchantId}
+          AND project_id = #{projectId}
+          AND is_delete = 0
+          -- 核心:只查询有效状态的订单 (待派单, 待接单, 待服务, 服务中)
+          AND status IN (1, 2, 3, 4)
+          -- 性能优化:限定查询当天的数据,避免全表扫描
+          AND appointment_start_time >= #{dayStart}
+          AND appointment_start_time &lt;= #{dayEnd}
+    </select>
 </mapper>