diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java index 043be566a..e4f0621c6 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryOrderStatus.java @@ -18,4 +18,8 @@ public final class CanteenDeliveryOrderStatus { public static final int COMPLETED = 3; /** 已取消 */ public static final int CANCELLED = 4; + /** + * 预约单:已到单但未达「提前 N 分钟派骑手」时间点,不展示在骑手待配送列表。 + */ + public static final int SCHEDULED_PENDING = 5; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java index 58b0d6d94..4b0465cf5 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/delivery/CanteenDeliveryOrderSyncDTO.java @@ -5,6 +5,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.List; /** @@ -59,4 +62,19 @@ public class CanteenDeliveryOrderSyncDTO { @Schema(description = "完整收货地址(展示用)") private String fullAddress; + + @Schema(description = "IMMEDIATE立即送(默认)| SCHEDULED预约时段;与硬件订单一致") + private String expectDeliveryType; + + @Schema(description = "预约送达日期(SCHEDULED 必填)") + private LocalDate expectDeliveryDate; + + @Schema(description = "预约时段开始(SCHEDULED 必填)") + private LocalTime expectDeliveryStart; + + @Schema(description = "预约时段结束(SCHEDULED 必填)") + private LocalTime expectDeliveryEnd; + + @Schema(description = "立即送预计送达时间(可选,如前端计算的「大约 09:36 送达」)") + private LocalDateTime expectArriveTime; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java index c9ec46c45..400c7ec36 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/entity/CanteenDeliveryOrderEntity.java @@ -7,7 +7,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; @Data @TenantTable @@ -37,6 +39,31 @@ public class CanteenDeliveryOrderEntity extends Model pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto); R deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto); + + /** + * 预约单:处理已到点的商家通知、骑手派单+推送。由 XXL-JOB 定时触发(见 {@link com.bsd.pjcloud.canteen.job.CanteenDeliveryScheduledJob})。 + */ + void runDueScheduledDeliveryTasks(); + + /** + * 单条:商家提醒(事务内行锁,幂等)。 + */ + void notifyMerchantIfDue(Long deliveryOrderId); + + /** + * 单条:骑手派单+推送(事务内行锁;无可用骑手时保持待调度状态下次重试)。 + */ + void dispatchScheduledRiderIfDue(Long deliveryOrderId); } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java index b1a8bd4b1..bf9952d1e 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDeliveryOrderServiceImpl.java @@ -7,9 +7,11 @@ import com.bsd.pjcloud.admin.api.dto.UserInfo; import com.bsd.pjcloud.admin.api.entity.SysDept; import com.bsd.pjcloud.admin.api.entity.SysUser; import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; +import com.bsd.pjcloud.admin.api.feign.RemoteParamService; import com.bsd.pjcloud.admin.api.feign.RemoteUserService; import com.bsd.pjcloud.canteen.constants.CanteenDeliveryAssignType; import com.bsd.pjcloud.canteen.constants.CanteenDeliveryOrderStatus; +import com.bsd.pjcloud.canteen.constants.CanteenDeliveryScheduleConstants; import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryCartItemDTO; import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO; import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryPhotoDTO; @@ -21,6 +23,7 @@ import com.bsd.pjcloud.canteen.mapper.CanteenDeliveryAssignSeqMapper; import com.bsd.pjcloud.canteen.mapper.CanteenDeliveryOrderMapper; import com.bsd.pjcloud.canteen.mapper.CanteenRiderBuildingBindMapper; import com.bsd.pjcloud.canteen.mapper.CanteenRiderPoolMapper; +import com.bsd.pjcloud.canteen.push.CanteenDeliveryMerchantNotifyPublisher; import com.bsd.pjcloud.canteen.push.CanteenDeliveryRiderPushPublisher; import com.bsd.pjcloud.canteen.service.CanteenDeliveryOrderService; import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO; @@ -32,15 +35,18 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; +@Slf4j @Service @RequiredArgsConstructor public class CanteenDeliveryOrderServiceImpl extends ServiceImpl @@ -53,6 +59,8 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl merchantDue = list(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getExpectDeliveryType, CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED) + .and(w -> w.eq(CanteenDeliveryOrderEntity::getMerchantNotifyDone, 0) + .or() + .isNull(CanteenDeliveryOrderEntity::getMerchantNotifyDone)) + .isNotNull(CanteenDeliveryOrderEntity::getScheduledMerchantNotifyTime) + .le(CanteenDeliveryOrderEntity::getScheduledMerchantNotifyTime, now) + .select(CanteenDeliveryOrderEntity::getId) + .last("LIMIT 500")); + for (CanteenDeliveryOrderEntity row : merchantDue) { + try { + notifyMerchantIfDue(row.getId()); + } catch (Exception ex) { + log.warn("notifyMerchantIfDue id={}", row.getId(), ex); + } + } + + List riderDue = list(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.SCHEDULED_PENDING) + .isNotNull(CanteenDeliveryOrderEntity::getScheduledRiderNotifyTime) + .le(CanteenDeliveryOrderEntity::getScheduledRiderNotifyTime, now) + .select(CanteenDeliveryOrderEntity::getId) + .last("LIMIT 500")); + for (CanteenDeliveryOrderEntity row : riderDue) { + try { + dispatchScheduledRiderIfDue(row.getId()); + } catch (Exception ex) { + log.warn("dispatchScheduledRiderIfDue id={}", row.getId(), ex); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void notifyMerchantIfDue(Long deliveryOrderId) { + if (deliveryOrderId == null) { + return; + } + CanteenDeliveryOrderEntity e = getOne(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getId, deliveryOrderId) + .last("FOR UPDATE")); + if (e == null) { + return; + } + if (!CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED.equalsIgnoreCase( + StrUtil.trim(StrUtil.emptyToDefault(e.getExpectDeliveryType(), "")))) { + return; + } + Integer done = e.getMerchantNotifyDone(); + if (done != null && done != 0) { + return; + } + if (e.getScheduledMerchantNotifyTime() == null || e.getScheduledMerchantNotifyTime().isAfter(LocalDateTime.now())) { + return; + } + merchantNotifyPublisher.publishScheduledOrderNotice(e); + e.setMerchantNotifyDone(1); + updateById(e); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void dispatchScheduledRiderIfDue(Long deliveryOrderId) { + if (deliveryOrderId == null) { + return; + } + CanteenDeliveryOrderEntity e = getOne(Wrappers.lambdaQuery() + .eq(CanteenDeliveryOrderEntity::getId, deliveryOrderId) + .last("FOR UPDATE")); + if (e == null || !Objects.equals(e.getStatus(), CanteenDeliveryOrderStatus.SCHEDULED_PENDING)) { + return; + } + if (e.getScheduledRiderNotifyTime() == null || e.getScheduledRiderNotifyTime().isAfter(LocalDateTime.now())) { + return; + } + assignRiderOnCreate(e); + if (e.getRiderUserId() == null) { + e.setStatus(CanteenDeliveryOrderStatus.SCHEDULED_PENDING); + updateById(e); + return; + } + updateById(e); + scheduleRiderPushAfterAssign(e); + } + + private int resolveNotifyBeforeMinutes(String paramKey, int defaultVal) { + try { + R r = remoteParamService.getByKey(paramKey); + if (r == null || !r.isOk() || StrUtil.isBlank(r.getData())) { + return defaultVal; + } + int v = Integer.parseInt(r.getData().trim()); + if (v < 0) { + return defaultVal; + } + if (v > 24 * 60) { + return 24 * 60; + } + return v; + } catch (Exception ex) { + return defaultVal; + } + } + /** * 来源订单号即硬件外卖订单主键(与同步时 dto.id 一致)。非数字则跳过(非硬件单)。 * 同步失败抛异常,触发事务回滚,避免本地状态与硬件订单不一致。 diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateOrderDto.java index bc7c3b51a..804a61b17 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateOrderDto.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateOrderDto.java @@ -11,6 +11,10 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + @Builder @AllArgsConstructor @NoArgsConstructor @@ -54,4 +58,15 @@ public class HardAppCreateOrderDto { // 地址 private HardAppTakeOutAddress address; + @Schema(description = "IMMEDIATE立即送 | SCHEDULED预约时段") + private String expectDeliveryType; + + private LocalDate expectDeliveryDate; + + private LocalTime expectDeliveryStart; + + private LocalTime expectDeliveryEnd; + + private LocalDateTime expectArriveTime; + } diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateTakeOutOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateTakeOutOrderDto.java index 37e3c6fbb..bb1b5a84f 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateTakeOutOrderDto.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/app/HardAppCreateTakeOutOrderDto.java @@ -1,8 +1,13 @@ package com.bsd.pjcloud.hard.api.dto.order.app; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + @Data public class HardAppCreateTakeOutOrderDto { @@ -23,4 +28,19 @@ public class HardAppCreateTakeOutOrderDto { @NotNull(message = "外卖地址不能为空") private HardAppTakeOutAddress address; + @Schema(description = "IMMEDIATE立即送(默认)| SCHEDULED预约时段") + private String expectDeliveryType; + + @Schema(description = "预约送达日期,SCHEDULED 时必填") + private LocalDate expectDeliveryDate; + + @Schema(description = "预约时段开始,SCHEDULED 时必填") + private LocalTime expectDeliveryStart; + + @Schema(description = "预约时段结束,SCHEDULED 时必填") + private LocalTime expectDeliveryEnd; + + @Schema(description = "立即送预计送达时间,可选") + private LocalDateTime expectArriveTime; + } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java index 8cafc4016..d64d44635 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java @@ -221,6 +221,11 @@ public class HardOrderController { .apptId(null) .address(dto.getAddress()) .deliveryPrice(dto.getDeliveryPrice()) + .expectDeliveryType(dto.getExpectDeliveryType()) + .expectDeliveryDate(dto.getExpectDeliveryDate()) + .expectDeliveryStart(dto.getExpectDeliveryStart()) + .expectDeliveryEnd(dto.getExpectDeliveryEnd()) + .expectArriveTime(dto.getExpectArriveTime()) .build(); return R.ok(hardwareOrderService.appCreateTakeOutOrder(createOrderDto)); } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index 9ba5381ee..ee5d9c97f 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -1,6 +1,7 @@ package com.bsd.pjcloud.hard.service.impl.order; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; @@ -600,6 +601,8 @@ public class HardOrderServiceImpl extends ServiceImpl