From a1eb855362dbe45de51ca185a0a93f7e3a1dccf6 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Mon, 13 Apr 2026 05:51:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E9=A2=84=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../constants/CanteenDeliveryOrderStatus.java | 4 + .../delivery/CanteenDeliveryOrderSyncDTO.java | 18 ++ .../entity/CanteenDeliveryOrderEntity.java | 27 +++ .../vo/app/CanteenDeliveryOrderAppVO.java | 10 ++ .../service/CanteenDeliveryOrderService.java | 15 ++ .../impl/CanteenDeliveryOrderServiceImpl.java | 156 ++++++++++++++++++ .../dto/order/app/HardAppCreateOrderDto.java | 15 ++ .../app/HardAppCreateTakeOutOrderDto.java | 20 +++ .../controller/order/HardOrderController.java | 5 + .../impl/order/HardOrderServiceImpl.java | 27 +++ 10 files changed, 297 insertions(+) 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 Date: Mon, 13 Apr 2026 14:23:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/order/app/HardAppCreateOrderDto.java | 17 +- .../app/HardAppCreateTakeOutOrderDto.java | 20 +-- .../app/HardTakeOutDeliveryTimeDayVO.java | 21 +++ .../app/HardTakeOutDeliveryTimeOptionVO.java | 48 +++++ .../app/HardTakeOutDeliveryTimeSlotVO.java | 23 +++ .../controller/order/HardOrderController.java | 19 ++ .../HardTakeOutDeliveryTimeOptionService.java | 21 +++ ...dTakeOutDeliveryTimeOptionServiceImpl.java | 118 +++++++++++++ .../impl/order/HardOrderServiceImpl.java | 74 +++++++- .../pjcloud/hard/util/MchJobTimeUtils.java | 167 ++++++++++++++++++ 10 files changed, 484 insertions(+), 44 deletions(-) create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeDayVO.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeOptionVO.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeSlotVO.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTakeOutDeliveryTimeOptionService.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardTakeOutDeliveryTimeOptionServiceImpl.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/util/MchJobTimeUtils.java 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 177c920e9..4e14ce098 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 @@ -10,10 +10,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; - @Builder @AllArgsConstructor @NoArgsConstructor @@ -57,18 +53,7 @@ public class HardAppCreateOrderDto { // 地址 private HardAppTakeOutAddressDto address; - // 预约时间信息 + // 预约时间信息(外卖:立即送/预约时段) private HardAppTakeOutApptDto appt; - @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 1ff290906..07d64ae4e 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 @@ -4,10 +4,6 @@ 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 { @@ -32,21 +28,7 @@ public class HardAppCreateTakeOutOrderDto { * 外卖预约信息 */ @NotNull(message = "预约时间信息不能为空") + @Schema(description = "立即送/预约时段;字段见 HardAppTakeOutApptDto") private HardAppTakeOutApptDto appt; - @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-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeDayVO.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeDayVO.java new file mode 100644 index 000000000..1a9d098d1 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeDayVO.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.hard.api.vo.order.app; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 某一配送日的可预约时段列表(多天查询时按天分组)。 + */ +@Data +@Schema(description = "单日可预约配送时段") +public class HardTakeOutDeliveryTimeDayVO { + + @Schema(description = "配送日") + private LocalDate expectDate; + + @Schema(description = "该日可选时段") + private List slots; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeOptionVO.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeOptionVO.java new file mode 100644 index 000000000..363151b4f --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeOptionVO.java @@ -0,0 +1,48 @@ +package com.bsd.pjcloud.hard.api.vo.order.app; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +/** + * 配送详情页:根据商户 {@code jobTime} 生成可预约时段;支持连续多天(今天/明天/后天等)。 + */ +@Data +@Schema(description = "外卖可预约配送时段(按商户营业时间切分)") +public class HardTakeOutDeliveryTimeOptionVO { + + @Schema(description = "商户ID") + private Long merchantId; + + @Schema(description = "商户名称") + private String merchantName; + + @Schema(description = "原始营业时间串,如 09:00-12:00,16:00-19:00") + private String jobTime; + + @Schema(description = "本次查询的起始配送日(未传 expectDate 时服务端为「今天」)") + private LocalDate startDate; + + @Schema(description = "连续查询的天数(含起始日),如 3 表示今天+明天+后天") + private Integer dayCount; + + @Schema(description = "每格时长(分钟)") + private Integer slotMinutes; + + @Schema(description = "「今天」时,过滤掉当前时刻之前不足 leadMinutes 分钟的格子;其它日期不按此过滤") + private Integer leadMinutes; + + @Schema(description = "jobTime 是否解析出有效营业时段") + private Boolean jobTimeValid; + + @Schema(description = "按天分组的时段;前端左侧「今天/明天」切换时取对应元素的 slots") + private List daySlots; + + /** + * 仅当 {@code dayCount == 1} 时与 {@link #daySlots} 中单日 slots 一致,便于旧前端兼容;多天时请使用 {@link #daySlots}。 + */ + @Schema(description = "单日查询时与 daySlots[0].slots 相同;多天时为 null") + private List slots; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeSlotVO.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeSlotVO.java new file mode 100644 index 000000000..224f22a8c --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/app/HardTakeOutDeliveryTimeSlotVO.java @@ -0,0 +1,23 @@ +package com.bsd.pjcloud.hard.api.vo.order.app; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalTime; + +/** + * 外卖预约可选时段(与 {@link com.bsd.pjcloud.hard.api.dto.order.app.HardAppTakeOutApptDto} 中 expectDeliveryStart/End 对应)。 + */ +@Data +@Schema(description = "外卖预约可选时段") +public class HardTakeOutDeliveryTimeSlotVO { + + @Schema(description = "时段开始") + @JsonFormat(pattern = "HH:mm:ss") + private LocalTime expectDeliveryStart; + + @Schema(description = "时段结束") + @JsonFormat(pattern = "HH:mm:ss") + private LocalTime expectDeliveryEnd; +} 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 0bc99c25f..a7b249470 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 @@ -40,12 +40,14 @@ import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateTakeOutOrderDto; import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateTakeOutStatusDto; import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeOptionVO; import com.bsd.pjcloud.hard.api.vo.order.excel.HardOrderExcelVo; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; import com.bsd.pjcloud.hard.api.vo.order.HardOnlineCanteenStatsVo; import com.bsd.pjcloud.hard.service.HardManagerService; import com.bsd.pjcloud.hard.service.HardOnlineCanteenStatsService; import com.bsd.pjcloud.hard.service.HardOrderService; +import com.bsd.pjcloud.hard.service.HardTakeOutDeliveryTimeOptionService; import com.bsd.pjcloud.hard.service.HardOrderSyncService; import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum; import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; @@ -60,8 +62,10 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.format.annotation.DateTimeFormat; import java.io.IOException; +import java.time.LocalDate; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; @@ -82,6 +86,7 @@ import java.util.Map; public class HardOrderController { private final HardOrderService hardwareOrderService; + private final HardTakeOutDeliveryTimeOptionService takeOutDeliveryTimeOptionService; private final HardManagerService hardManagerService; private final HardOnlineCanteenStatsService hardOnlineCanteenStatsService; private final HardOrderSyncService hardOrderSyncService; @@ -206,6 +211,20 @@ public class HardOrderController { return R.ok(hardwareOrderService.appCreateOrder(dto)); } + @Operation(summary = "外卖-查询可预约配送时段(支持连续多天)", + description = "配送详情页:按商户 MchManage.jobTime 切分时段;与下单校验一致。" + + "不传 expectDate 时从「今天」起算;days 为连续天数(含起始日),如 days=3 表示今天+明天+后天。" + + "仅「日历日为今天」时按 leadMinutes 过滤已过近的格子。") + @GetMapping("/app/take-out/delivery-time-slots") + public R takeOutDeliveryTimeSlots( + @RequestParam Long merchantId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate expectDate, + @RequestParam(required = false) Integer days, + @RequestParam(required = false) Integer slotMinutes, + @RequestParam(required = false) Integer leadMinutes) { + return takeOutDeliveryTimeOptionService.listDeliveryTimeSlots(merchantId, expectDate, days, slotMinutes, leadMinutes); + } + @Operation(summary = "app创建食堂外卖订单", description = "app创建食堂外卖订单") @PostMapping("/app/create-take-out-order") @Idempotent(expireTime = 3, key = "'/app/hard-order/app/create-take-out-order' + #dto.getCarParentId() ", info = "正在创建订单中,请勿重复操作") diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTakeOutDeliveryTimeOptionService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTakeOutDeliveryTimeOptionService.java new file mode 100644 index 000000000..a5e6dfdc1 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTakeOutDeliveryTimeOptionService.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.hard.service; + +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeOptionVO; + +import java.time.LocalDate; + +/** + * 外卖配送详情:按商户 {@code jobTime} 生成可下单预约时段。 + */ +public interface HardTakeOutDeliveryTimeOptionService { + + /** + * @param merchantId 商户主键 + * @param expectDate 起始配送日,{@code null} 表示从「今天」起算 + * @param days 连续天数(含起始日),默认 1,最大 14;如 3 表示今天+明天+后天(起始日为今天时) + * @param slotMinutes 切分步长(分钟),默认 30 + * @param leadMinutes 仅对「日历日为今天」的格子生效:去掉当前时刻 + leadMinutes 之前的格子;默认 30 + */ + R listDeliveryTimeSlots(Long merchantId, LocalDate expectDate, Integer days, Integer slotMinutes, Integer leadMinutes); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardTakeOutDeliveryTimeOptionServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardTakeOutDeliveryTimeOptionServiceImpl.java new file mode 100644 index 000000000..a016a4c8a --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardTakeOutDeliveryTimeOptionServiceImpl.java @@ -0,0 +1,118 @@ +package com.bsd.pjcloud.hard.service.impl; + +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeDayVO; +import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeOptionVO; +import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeSlotVO; +import com.bsd.pjcloud.hard.service.HardTakeOutDeliveryTimeOptionService; +import com.bsd.pjcloud.hard.util.MchJobTimeUtils; +import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; +import com.bsd.pjcloud.wallet.api.vo.MchManageVo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class HardTakeOutDeliveryTimeOptionServiceImpl implements HardTakeOutDeliveryTimeOptionService { + + private static final int DEFAULT_SLOT_MINUTES = 30; + private static final int DEFAULT_LEAD_MINUTES = 30; + private static final int DEFAULT_DAY_COUNT = 1; + private static final int MAX_DAY_COUNT = 14; + private static final int MAX_SLOT_MINUTES = 24 * 60; + + private final RemoteMchService remoteMchService; + + @Override + public R listDeliveryTimeSlots(Long merchantId, LocalDate expectDate, Integer days, Integer slotMinutes, Integer leadMinutes) { + if (merchantId == null) { + return R.failed("商户ID不能为空"); + } + LocalDate today = LocalDate.now(); + LocalDate startDate = expectDate != null ? expectDate : today; + if (startDate.isBefore(today)) { + return R.failed("不能选择过去的日期"); + } + int dayCount = days == null || days < 1 ? DEFAULT_DAY_COUNT : Math.min(days, MAX_DAY_COUNT); + int sm = slotMinutes == null || slotMinutes <= 0 ? DEFAULT_SLOT_MINUTES : Math.min(slotMinutes, MAX_SLOT_MINUTES); + int lm = leadMinutes == null || leadMinutes < 0 ? DEFAULT_LEAD_MINUTES : leadMinutes; + + R r = remoteMchService.getById(merchantId); + if (r == null || !r.isOk() || r.getData() == null) { + return R.failed("商户不存在"); + } + MchManageVo mch = r.getData(); + String jobTime = mch.getJobTime(); + + HardTakeOutDeliveryTimeOptionVO vo = new HardTakeOutDeliveryTimeOptionVO(); + vo.setMerchantId(merchantId); + vo.setMerchantName(mch.getName()); + vo.setJobTime(jobTime); + vo.setStartDate(startDate); + vo.setDayCount(dayCount); + vo.setSlotMinutes(sm); + vo.setLeadMinutes(lm); + + if (MchJobTimeUtils.isBlankJobTime(jobTime)) { + vo.setJobTimeValid(false); + vo.setDaySlots(List.of()); + vo.setSlots(List.of()); + return R.ok(vo); + } + + List ranges = MchJobTimeUtils.splitJobTimeIntoSlots(jobTime, sm); + if (ranges.isEmpty()) { + vo.setJobTimeValid(false); + vo.setDaySlots(List.of()); + vo.setSlots(List.of()); + return R.ok(vo); + } + + vo.setJobTimeValid(true); + List daySlots = new ArrayList<>(dayCount); + for (int i = 0; i < dayCount; i++) { + LocalDate d = startDate.plusDays(i); + HardTakeOutDeliveryTimeDayVO dayVo = new HardTakeOutDeliveryTimeDayVO(); + dayVo.setExpectDate(d); + dayVo.setSlots(buildSlotsForCalendarDay(d, ranges, today, lm)); + daySlots.add(dayVo); + } + vo.setDaySlots(daySlots); + if (dayCount == 1 && daySlots.size() == 1) { + vo.setSlots(daySlots.get(0).getSlots()); + } else { + vo.setSlots(null); + } + return R.ok(vo); + } + + private static List buildSlotsForCalendarDay( + LocalDate expectDate, + List ranges, + LocalDate today, + int leadMinutes) { + LocalDateTime cutoff = null; + if (expectDate.equals(today)) { + cutoff = LocalDateTime.now().plusMinutes(leadMinutes); + } + List slots = new ArrayList<>(ranges.size()); + for (MchJobTimeUtils.TimeRange tr : ranges) { + if (cutoff != null) { + LocalDateTime slotStart = LocalDateTime.of(expectDate, tr.start); + if (slotStart.isBefore(cutoff)) { + continue; + } + } + HardTakeOutDeliveryTimeSlotVO row = new HardTakeOutDeliveryTimeSlotVO(); + row.setExpectDeliveryStart(tr.start); + row.setExpectDeliveryEnd(tr.end); + slots.add(row); + } + return slots; + } +} 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 1c11ecee8..3689f38e1 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 @@ -55,6 +55,7 @@ import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.dto.order.act.HardCreateActOrderDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateFarmerOrderDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateOrderDto; +import com.bsd.pjcloud.hard.api.dto.order.app.HardAppTakeOutApptDto; import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateTakeOutStatusDto; import com.bsd.pjcloud.hard.api.dto.order.xfj.HardXfjFastCreateOrderDto; import com.bsd.pjcloud.hard.api.dto.spu.DeductionSpuStockDto; @@ -71,6 +72,7 @@ import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo; import com.bsd.pjcloud.hard.api.vo.order.HardCarVo; import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.util.MchJobTimeUtils; import com.bsd.pjcloud.hard.api.vo.wechat.WechatMicropayVo; import com.bsd.pjcloud.hard.mapper.HardThirdMiOrderMapper; import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper; @@ -627,6 +629,7 @@ public class HardOrderServiceImpl extends ServiceImpl carList = hardCarService.getByParentCarId(dto.getCarParentId()); if (ObjectUtil.isEmpty(carList)) throw new RuntimeException("购物车不存在"); @@ -729,11 +732,7 @@ public class HardOrderServiceImpl extends ServiceImpl merged = mergeJobTimeIntervals(jobTime); + if (merged.isEmpty()) { + return false; + } + for (TimeRange r : merged) { + if (!r.start.isAfter(slotStart) && !r.end.isBefore(slotEnd)) { + return true; + } + } + return false; + } + + /** + * 单个时刻(如立即送预计送达的时间部分)是否落在营业时间内。 + */ + public static boolean isTimeWithinJobTime(String jobTime, LocalTime t) { + if (t == null) { + return false; + } + List merged = mergeJobTimeIntervals(jobTime); + for (TimeRange r : merged) { + if (!t.isBefore(r.start) && !t.isAfter(r.end)) { + return true; + } + } + return false; + } + + /** + * 解析并合并相邻或重叠的时段(如 09:00-12:00 与 12:00-14:00 合并为 09:00-14:00)。 + */ + public static List mergeJobTimeIntervals(String jobTime) { + List raw = new ArrayList<>(); + if (isBlankJobTime(jobTime)) { + return raw; + } + for (String part : jobTime.split(",")) { + part = StrUtil.trim(part); + if (StrUtil.isBlank(part)) { + continue; + } + String[] se = part.split("-"); + if (se.length != 2) { + continue; + } + LocalTime a = parseHm(StrUtil.trim(se[0])); + LocalTime b = parseHm(StrUtil.trim(se[1])); + if (a == null || b == null) { + continue; + } + if (!b.isAfter(a)) { + continue; + } + raw.add(new TimeRange(a, b)); + } + if (raw.isEmpty()) { + return raw; + } + raw.sort(Comparator.comparing(r -> r.start)); + List merged = new ArrayList<>(); + TimeRange cur = raw.get(0); + for (int i = 1; i < raw.size(); i++) { + TimeRange next = raw.get(i); + if (!next.start.isAfter(cur.end)) { + if (next.end.isAfter(cur.end)) { + cur = new TimeRange(cur.start, next.end); + } + } else { + merged.add(cur); + cur = next; + } + } + merged.add(cur); + return merged; + } + + /** + * 将合并后的营业时段按固定步长切成预约格子(整段落在原时段内;最后不足一个步长则丢弃)。 + * + * @param jobTime 商户 jobTime 原始串 + * @param slotMinutes 每格时长(分钟),如 30 + */ + public static List splitJobTimeIntoSlots(String jobTime, int slotMinutes) { + if (slotMinutes <= 0) { + return List.of(); + } + List merged = mergeJobTimeIntervals(jobTime); + List out = new ArrayList<>(); + for (TimeRange r : merged) { + LocalTime cursor = r.start; + while (true) { + LocalTime slotEnd = cursor.plusMinutes(slotMinutes); + if (slotEnd.isAfter(r.end)) { + break; + } + out.add(new TimeRange(cursor, slotEnd)); + cursor = slotEnd; + } + } + return out; + } + + private static LocalTime parseHm(String s) { + try { + return LocalTime.parse(s, HM_OPTIONAL_SEC); + } catch (DateTimeParseException e) { + try { + return LocalTime.parse(s, HM); + } catch (DateTimeParseException e2) { + return null; + } + } + } + + public static final class TimeRange { + public final LocalTime start; + public final LocalTime end; + + public TimeRange(LocalTime start, LocalTime end) { + this.start = start; + this.end = end; + } + } +}