订单部分代码

This commit is contained in:
zjf committed 2026-04-13 14:23:35 +08:00
1 parent 6cdc873a02
commit bfa5dc6a26
10 files changed
+484 -44

No files matched your search

@@ -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;
}
@@ -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;
}
@@ -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<HardTakeOutDeliveryTimeSlotVO> slots;
}
@@ -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<HardTakeOutDeliveryTimeDayVO> daySlots;
/**
* 仅当 {@code dayCount == 1} 时与 {@link #daySlots} 中单日 slots 一致,便于旧前端兼容;多天时请使用 {@link #daySlots}。
*/
@Schema(description = "单日查询时与 daySlots[0].slots 相同;多天时为 null")
private List<HardTakeOutDeliveryTimeSlotVO> slots;
}
@@ -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;
}
@@ -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<HardTakeOutDeliveryTimeOptionVO> 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 = "正在创建订单中,请勿重复操作")
@@ -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<HardTakeOutDeliveryTimeOptionVO> listDeliveryTimeSlots(Long merchantId, LocalDate expectDate, Integer days, Integer slotMinutes, Integer leadMinutes);
}
@@ -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<HardTakeOutDeliveryTimeOptionVO> 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<MchManageVo> 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<MchJobTimeUtils.TimeRange> 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<HardTakeOutDeliveryTimeDayVO> 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<HardTakeOutDeliveryTimeSlotVO> buildSlotsForCalendarDay(
LocalDate expectDate,
List<MchJobTimeUtils.TimeRange> ranges,
LocalDate today,
int leadMinutes) {
LocalDateTime cutoff = null;
if (expectDate.equals(today)) {
cutoff = LocalDateTime.now().plusMinutes(leadMinutes);
}
List<HardTakeOutDeliveryTimeSlotVO> 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;
}
}
@@ -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<HardOrderMapper, HardOrder
// 查询商户
MchManageVo hardManager = remoteMchService.getById(parentCar.getMerchantId()).getData();
if (ObjectUtil.isEmpty(hardManager)) throw new RuntimeException("商户不存在");
validateTakeOutApptAgainstMerchantJobTime(hardManager, dto.getAppt());
// 查询父购物车
List<HardCarEntity> carList = hardCarService.getByParentCarId(dto.getCarParentId());
if (ObjectUtil.isEmpty(carList)) throw new RuntimeException("购物车不存在");
@@ -729,11 +732,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
canteenDeliveryOrderSyncDTO.setId(String.valueOf(orderObj.getId()));
canteenDeliveryOrderSyncDTO.setFullAddress(dto.getAddress().getFullAddress());
canteenDeliveryOrderSyncDTO.setBuildingId(dto.getAddress().getBuildingId());
canteenDeliveryOrderSyncDTO.setExpectDeliveryType(StrUtil.emptyToDefault(dto.getExpectDeliveryType(), "IMMEDIATE"));
canteenDeliveryOrderSyncDTO.setExpectDeliveryDate(dto.getExpectDeliveryDate());
canteenDeliveryOrderSyncDTO.setExpectDeliveryStart(dto.getExpectDeliveryStart());
canteenDeliveryOrderSyncDTO.setExpectDeliveryEnd(dto.getExpectDeliveryEnd());
canteenDeliveryOrderSyncDTO.setExpectArriveTime(dto.getExpectArriveTime());
fillCanteenExpectDeliveryFromAppt(canteenDeliveryOrderSyncDTO, dto.getAppt());
remoteCanteenDeliveryOrderService.syncFromOrder(canteenDeliveryOrderSyncDTO);
return orderObj.getId();
@@ -1897,24 +1896,81 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
/**
* 外卖预约配送:SCHEDULED 须带日期与时段;立即送默认 IMMEDIATE。
* 预约/立即送时间须落在商户 {@link com.bsd.pjcloud.wallet.api.entity.mch.MchManageEntity#getJobTime()} 营业时段内(如 {@code 09:00-12:00,16:00-19:00})。
*/
private void validateTakeOutApptAgainstMerchantJobTime(MchManageVo merchant, HardAppTakeOutApptDto appt) {
if (appt == null || merchant == null) {
return;
}
String type = StrUtil.emptyToDefault(appt.getExpectDeliveryType(), "IMMEDIATE").trim();
if ("IMMEDIATE".equalsIgnoreCase(type)) {
if (appt.getExpectArriveTime() != null) {
if (MchJobTimeUtils.isBlankJobTime(merchant.getJobTime())) {
throw new RuntimeException("商户未配置营业时间");
}
if (!MchJobTimeUtils.isTimeWithinJobTime(merchant.getJobTime(), appt.getExpectArriveTime().toLocalTime())) {
throw new RuntimeException("预计送达时间不在商户营业时间内,请重新选择");
}
}
return;
}
if (!"SCHEDULED".equalsIgnoreCase(type)) {
return;
}
if (MchJobTimeUtils.isBlankJobTime(merchant.getJobTime())) {
throw new RuntimeException("商户未配置营业时间,暂不可预约配送");
}
if (MchJobTimeUtils.mergeJobTimeIntervals(merchant.getJobTime()).isEmpty()) {
throw new RuntimeException("商户营业时间格式无效,请先在后台正确配置 jobTime");
}
if (!MchJobTimeUtils.isDeliverySlotWithinJobTime(merchant.getJobTime(), appt.getExpectDeliveryStart(), appt.getExpectDeliveryEnd())) {
throw new RuntimeException("预约时段不在商户营业时间内,请重新选择");
}
}
/**
* 外卖预约配送:字段在 {@link HardAppTakeOutApptDto};SCHEDULED 须带日期与时段;立即送默认 IMMEDIATE。
*/
private void validateTakeOutExpectDelivery(HardAppCreateOrderDto dto) {
String t = dto.getExpectDeliveryType();
if (dto.getAppt() == null) {
return;
}
HardAppTakeOutApptDto a = dto.getAppt();
String t = StrUtil.emptyToDefault(a.getExpectDeliveryType(), "IMMEDIATE");
if (StrUtil.isBlank(t) || "IMMEDIATE".equalsIgnoreCase(t.trim())) {
return;
}
if (!"SCHEDULED".equalsIgnoreCase(t.trim())) {
throw new RuntimeException("配送时间类型无效");
}
if (dto.getExpectDeliveryDate() == null || dto.getExpectDeliveryStart() == null || dto.getExpectDeliveryEnd() == null) {
if (a.getExpectDeliveryDate() == null || a.getExpectDeliveryStart() == null || a.getExpectDeliveryEnd() == null) {
throw new RuntimeException("预约配送须选择日期与时段");
}
if (!dto.getExpectDeliveryEnd().isAfter(dto.getExpectDeliveryStart())) {
if (!a.getExpectDeliveryEnd().isAfter(a.getExpectDeliveryStart())) {
throw new RuntimeException("预约时段结束须晚于开始");
}
}
/**
* 将外卖请求中的 {@code appt} 写入食堂外送同步 DTO(与 {@link HardTakeOutApptEntity} 语义一致)。
*/
private void fillCanteenExpectDeliveryFromAppt(CanteenDeliveryOrderSyncDTO sync, HardAppTakeOutApptDto appt) {
if (appt == null) {
sync.setExpectDeliveryType("IMMEDIATE");
return;
}
String raw = StrUtil.emptyToDefault(appt.getExpectDeliveryType(), "IMMEDIATE").trim();
if ("SCHEDULED".equalsIgnoreCase(raw)) {
sync.setExpectDeliveryType("SCHEDULED");
} else {
sync.setExpectDeliveryType("IMMEDIATE");
}
sync.setExpectDeliveryDate(appt.getExpectDeliveryDate());
sync.setExpectDeliveryStart(appt.getExpectDeliveryStart());
sync.setExpectDeliveryEnd(appt.getExpectDeliveryEnd());
sync.setExpectArriveTime(appt.getExpectArriveTime());
}
// 是否在预约时间段内
private boolean isInReserveTime(Long apptId) {
if (ObjectUtil.isEmpty(apptId)) return true;
@@ -0,0 +1,167 @@
package com.bsd.pjcloud.hard.util;
import cn.hutool.core.util.StrUtil;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* 商户 {@code jobTime} 解析:如 {@code "09:00-12:00,12:00-14:00,16:00-19:00"},用于校验预约/预计送达时刻是否在营业时间内。
*/
public final class MchJobTimeUtils {
private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm");
/** 兼容 jobTime 配置与前端 LocalTime 序列化(可带秒) */
private static final DateTimeFormatter HM_OPTIONAL_SEC = new DateTimeFormatterBuilder()
.appendPattern("HH:mm")
.optionalStart()
.appendPattern(":ss")
.optionalEnd()
.toFormatter();
private MchJobTimeUtils() {
}
public static boolean isBlankJobTime(String jobTime) {
return StrUtil.isBlank(StrUtil.trim(jobTime));
}
/**
* 预约配送时段 [slotStart, slotEnd] 是否完全落在某一合并后的营业时段内(闭区间,含边界)。
*/
public static boolean isDeliverySlotWithinJobTime(String jobTime, LocalTime slotStart, LocalTime slotEnd) {
if (slotStart == null || slotEnd == null) {
return false;
}
if (!slotEnd.isAfter(slotStart)) {
return false;
}
List<TimeRange> 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<TimeRange> 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<TimeRange> mergeJobTimeIntervals(String jobTime) {
List<TimeRange> 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<TimeRange> 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<TimeRange> splitJobTimeIntoSlots(String jobTime, int slotMinutes) {
if (slotMinutes <= 0) {
return List.of();
}
List<TimeRange> merged = mergeJobTimeIntervals(jobTime);
List<TimeRange> 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;
}
}
}