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 7229ed61c..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 @@ -53,7 +53,7 @@ public class HardAppCreateOrderDto { // 地址 private HardAppTakeOutAddressDto address; - // 预约时间信息 + // 预约时间信息(外卖:立即送/预约时段) private HardAppTakeOutApptDto appt; } 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 83b11379b..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 @@ -1,5 +1,6 @@ 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; @@ -27,6 +28,7 @@ public class HardAppCreateTakeOutOrderDto { * 外卖预约信息 */ @NotNull(message = "预约时间信息不能为空") + @Schema(description = "立即送/预约时段;字段见 HardAppTakeOutApptDto") private HardAppTakeOutApptDto appt; } 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 34f26193c..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 @@ -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; @@ -54,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; @@ -70,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; @@ -602,6 +605,8 @@ public class HardOrderServiceImpl extends ServiceImpl carList = hardCarService.getByParentCarId(dto.getCarParentId()); if (ObjectUtil.isEmpty(carList)) throw new RuntimeException("购物车不存在"); @@ -726,6 +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; + } + } +}