This commit is contained in:
zjf committed 2026-04-13 05:51:22 +08:00
1 parent d23d518629
commit a1eb855362
10 files changed
+297

No files matched your search

@@ -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;
}
@@ -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;
}
@@ -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<CanteenDeliveryOrderEntity
private String assignType;
private Long riderUserId;
private Integer status;
@Schema(description = "IMMEDIATE立即送 SCHEDULED预约时段")
private String expectDeliveryType;
@Schema(description = "预约送达日期(仅 SCHEDULED)")
private LocalDate expectDeliveryDate;
@Schema(description = "预约时段开始")
private LocalTime expectDeliveryStart;
@Schema(description = "预约时段结束")
private LocalTime expectDeliveryEnd;
@Schema(description = "立即送时预计送达时间(可选)")
private LocalDateTime expectArriveTime;
@Schema(description = "计划向骑手派单+推送时间(预约单;= 时段开始 - 配置分钟)")
private LocalDateTime scheduledRiderNotifyTime;
@Schema(description = "计划通知商家时间(预约单;= 时段开始 - 配置分钟)")
private LocalDateTime scheduledMerchantNotifyTime;
@Schema(description = "0 待通知商家 1 已通知(预约单)")
private Integer merchantNotifyDone;
private String pickupPhotoUrl;
private String deliveryPhotoUrl;
@@ -4,7 +4,9 @@ import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryCartItemDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
@Data
@@ -21,6 +23,14 @@ public class CanteenDeliveryOrderAppVO {
private String merchantLogo;
private String fullAddress;
private Integer status;
@Schema(description = "IMMEDIATE立即送 SCHEDULED预约时段")
private String expectDeliveryType;
private LocalDate expectDeliveryDate;
private LocalTime expectDeliveryStart;
private LocalTime expectDeliveryEnd;
private LocalDateTime expectArriveTime;
private String pickupPhotoUrl;
private String deliveryPhotoUrl;
private LocalDateTime createTime;
@@ -28,4 +28,19 @@ public interface CanteenDeliveryOrderService extends IService<CanteenDeliveryOrd
R<Void> pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto);
R<Void> deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto);
/**
* 预约单:处理已到点的商家通知、骑手派单+推送。由 XXL-JOB 定时触发(见 {@link com.bsd.pjcloud.canteen.job.CanteenDeliveryScheduledJob})。
*/
void runDueScheduledDeliveryTasks();
/**
* 单条:商家提醒(事务内行锁,幂等)。
*/
void notifyMerchantIfDue(Long deliveryOrderId);
/**
* 单条:骑手派单+推送(事务内行锁;无可用骑手时保持待调度状态下次重试)。
*/
void dispatchScheduledRiderIfDue(Long deliveryOrderId);
}
@@ -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<CanteenDeliveryOrderMapper, CanteenDeliveryOrderEntity>
@@ -53,6 +59,8 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
private final RemoteDeptService remoteDeptService;
private final ObjectMapper objectMapper;
private final CanteenDeliveryRiderPushPublisher riderPushPublisher;
private final CanteenDeliveryMerchantNotifyPublisher merchantNotifyPublisher;
private final RemoteParamService remoteParamService;
private final RemoteHardOrderService remoteHardOrderService;
@Override
@@ -89,11 +97,52 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
entity.setMerchantLogo(dto.getLogoPic());
entity.setBuildingId(dto.getBuildingId());
entity.setFullAddress(dto.getFullAddress());
String rawExpectType = StrUtil.emptyToDefault(dto.getExpectDeliveryType(), CanteenDeliveryScheduleConstants.EXPECT_TYPE_IMMEDIATE).trim();
if (CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED.equalsIgnoreCase(rawExpectType)) {
entity.setExpectDeliveryType(CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED);
} else {
entity.setExpectDeliveryType(CanteenDeliveryScheduleConstants.EXPECT_TYPE_IMMEDIATE);
}
entity.setExpectDeliveryDate(dto.getExpectDeliveryDate());
entity.setExpectDeliveryStart(dto.getExpectDeliveryStart());
entity.setExpectDeliveryEnd(dto.getExpectDeliveryEnd());
entity.setExpectArriveTime(dto.getExpectArriveTime());
try {
entity.setCarListJson(objectMapper.writeValueAsString(dto.getCarList()));
} catch (JsonProcessingException e) {
return R.failed("购物车序列化失败");
}
String expectType = entity.getExpectDeliveryType();
if (!CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED.equals(expectType)
|| entity.getExpectDeliveryDate() == null || entity.getExpectDeliveryStart() == null) {
entity.setMerchantNotifyDone(1);
entity.setScheduledMerchantNotifyTime(null);
entity.setScheduledRiderNotifyTime(null);
assignRiderOnCreate(entity);
save(entity);
scheduleRiderPushAfterAssign(entity);
return R.ok(entity.getId());
}
LocalDateTime now = LocalDateTime.now();
LocalDateTime slotStart = LocalDateTime.of(entity.getExpectDeliveryDate(), entity.getExpectDeliveryStart());
int riderBefore = resolveNotifyBeforeMinutes(CanteenDeliveryScheduleConstants.PARAM_RIDER_NOTIFY_BEFORE_MINUTES,
CanteenDeliveryScheduleConstants.DEFAULT_RIDER_NOTIFY_BEFORE_MINUTES);
int merchantBefore = resolveNotifyBeforeMinutes(CanteenDeliveryScheduleConstants.PARAM_MERCHANT_NOTIFY_BEFORE_MINUTES,
CanteenDeliveryScheduleConstants.DEFAULT_MERCHANT_NOTIFY_BEFORE_MINUTES);
entity.setScheduledRiderNotifyTime(slotStart.minusMinutes(riderBefore));
entity.setScheduledMerchantNotifyTime(slotStart.minusMinutes(merchantBefore));
entity.setMerchantNotifyDone(0);
if (entity.getScheduledRiderNotifyTime().isAfter(now)) {
entity.setStatus(CanteenDeliveryOrderStatus.SCHEDULED_PENDING);
entity.setAssignType(CanteenDeliveryAssignType.NONE);
entity.setRiderUserId(null);
save(entity);
return R.ok(entity.getId());
}
assignRiderOnCreate(entity);
save(entity);
scheduleRiderPushAfterAssign(entity);
@@ -334,6 +383,113 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
return R.ok();
}
@Override
public void runDueScheduledDeliveryTasks() {
LocalDateTime now = LocalDateTime.now();
List<CanteenDeliveryOrderEntity> merchantDue = list(Wrappers.<CanteenDeliveryOrderEntity>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<CanteenDeliveryOrderEntity> riderDue = list(Wrappers.<CanteenDeliveryOrderEntity>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.<CanteenDeliveryOrderEntity>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.<CanteenDeliveryOrderEntity>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<String> 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 一致)。非数字则跳过(非硬件单)。
* 同步失败抛异常,触发事务回滚,避免本地状态与硬件订单不一致。
@@ -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;
}
@@ -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;
}
@@ -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));
}
@@ -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<HardOrderMapper, HardOrder
UserInfo userDetail = userService.getInnerByUserId(dto.getUserId()).getData();
if (ObjectUtil.isEmpty(userDetail)) throw new RuntimeException("用户不存在不存在");
validateTakeOutExpectDelivery(dto);
// 如果是预约判断是否在预约时间段内
boolean inReserveTime = isInReserveTime(dto.getApptId());
if (!inReserveTime) throw new RuntimeException("不在预约时间段内");
@@ -712,6 +715,11 @@ 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());
remoteCanteenDeliveryOrderService.syncFromOrder(canteenDeliveryOrderSyncDTO);
return orderObj.getId();
@@ -1792,6 +1800,25 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
);
}
/**
* 外卖预约配送:SCHEDULED 须带日期与时段;立即送默认 IMMEDIATE。
*/
private void validateTakeOutExpectDelivery(HardAppCreateOrderDto dto) {
String t = dto.getExpectDeliveryType();
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) {
throw new RuntimeException("预约配送须选择日期与时段");
}
if (!dto.getExpectDeliveryEnd().isAfter(dto.getExpectDeliveryStart())) {
throw new RuntimeException("预约时段结束须晚于开始");
}
}
// 是否在预约时间段内
private boolean isInReserveTime(Long apptId) {
if (ObjectUtil.isEmpty(apptId)) return true;