商家外卖订单
This commit is contained in:
1 parent
e181483644
commit
371c012498
7 files changed
+592
-13
No files matched your search
+11
-1
@@ -1,7 +1,7 @@
|
||||
package com.bsd.pjcloud.canteen.constants;
|
||||
|
||||
/**
|
||||
* 食堂外送 — 推送给骑手的 WebSocket 业务类型(messageText JSON 内字段 bizType)
|
||||
* 食堂外送 — 推送给骑手 / 商家端 App 的 WebSocket 业务类型(messageText JSON 内字段 bizType)
|
||||
*/
|
||||
public final class CanteenDeliveryPushConstants {
|
||||
|
||||
@@ -13,6 +13,16 @@ public final class CanteenDeliveryPushConstants {
|
||||
*/
|
||||
public static final String BIZ_TYPE_ORDER_ASSIGNED = "CANTEEN_DELIVERY_ORDER_ASSIGNED";
|
||||
|
||||
/**
|
||||
* 预约外送到达「通知商家」时间点(定时任务;推送给商户部门链上的用户)
|
||||
*/
|
||||
public static final String BIZ_TYPE_SCHEDULED_MERCHANT_REMIND = "CANTEEN_DELIVERY_SCHEDULED_MERCHANT_REMIND";
|
||||
|
||||
/**
|
||||
* 支付同步落库后立即通知商家(立即送;商家需知晓新单)
|
||||
*/
|
||||
public static final String BIZ_TYPE_MERCHANT_NEW_ORDER = "CANTEEN_DELIVERY_MERCHANT_NEW_ORDER";
|
||||
|
||||
/** messageText 内 schema 版本,前端兼容用 */
|
||||
public static final int PAYLOAD_VERSION = 1;
|
||||
}
|
||||
+10
-1
@@ -10,7 +10,7 @@ import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "骑手端外送单摘要")
|
||||
@Schema(description = "外送单 App 视图(骑手端 / 商家端共用;商家端展示骑手联系信息)")
|
||||
public class CanteenDeliveryOrderAppVO {
|
||||
|
||||
private Long id;
|
||||
@@ -24,6 +24,15 @@ public class CanteenDeliveryOrderAppVO {
|
||||
private String fullAddress;
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "指派骑手用户主键(未指派时为 null)")
|
||||
private Long riderUserId;
|
||||
|
||||
@Schema(description = "骑手姓名(商家端列表/详情;来自用户中心,未指派或查询失败为 null)")
|
||||
private String riderName;
|
||||
|
||||
@Schema(description = "骑手手机号(商家端列表/详情;来自用户中心,未指派或查询失败为 null)")
|
||||
private String riderMobile;
|
||||
|
||||
@Schema(description = "IMMEDIATE立即送 SCHEDULED预约时段")
|
||||
private String expectDeliveryType;
|
||||
private LocalDate expectDeliveryDate;
|
||||
|
||||
+47
-1
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.canteen.controller;
|
||||
|
||||
import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryOrderSyncDTO;
|
||||
import com.bsd.pjcloud.canteen.dto.delivery.CanteenDeliveryPhotoDTO;
|
||||
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.entity.CanteenDeliveryOrderEntity;
|
||||
@@ -35,6 +36,8 @@ public class CanteenDeliveryOrderController {
|
||||
|
||||
private final CanteenDeliveryRiderPushPublisher riderPushPublisher;
|
||||
|
||||
private final CanteenDeliveryMerchantNotifyPublisher merchantNotifyPublisher;
|
||||
|
||||
/**
|
||||
* 联调:模拟「新单已指派」的 WebSocket 推送(与正式推送同一 Redis 频道与 messageText 结构)。
|
||||
* 骑手端须已用相同 userId 建立 WS;需管理端权限 {@code canteen_canteenDelivery_config}。
|
||||
@@ -50,10 +53,36 @@ public class CanteenDeliveryOrderController {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@SysLog("测试商家WS推送")
|
||||
@PostMapping("/test/mock-merchant-push")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')")
|
||||
@Operation(summary = "测试-模拟向商家端推送预约提醒 WS 消息",
|
||||
description = "参数 merchantStaffUserId 为商家端登录用户主键(与 WS sessionKey 一致);deliveryOrderId 可选,不传则用时间戳占位")
|
||||
public R<Void> mockMerchantPush(@RequestParam Long merchantStaffUserId,
|
||||
@RequestParam(required = false) Long deliveryOrderId) {
|
||||
long fakeId = deliveryOrderId != null ? deliveryOrderId : System.currentTimeMillis();
|
||||
merchantNotifyPublisher.publishScheduledRemindMock(
|
||||
merchantStaffUserId, fakeId, "MOCK-TEST-" + fakeId, "【测试】食堂", "【测试】收货地址");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@SysLog("测试商家新单WS推送")
|
||||
@PostMapping("/test/mock-merchant-new-order-push")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenDelivery_config')")
|
||||
@Operation(summary = "测试-模拟立即送新单商家 WS(bizType=MERCHANT_NEW_ORDER)",
|
||||
description = "参数 merchantStaffUserId 为商家端登录用户主键;deliveryOrderId 可选")
|
||||
public R<Void> mockMerchantNewOrderPush(@RequestParam Long merchantStaffUserId,
|
||||
@RequestParam(required = false) Long deliveryOrderId) {
|
||||
long fakeId = deliveryOrderId != null ? deliveryOrderId : System.currentTimeMillis();
|
||||
merchantNotifyPublisher.publishMerchantNewOrderMock(
|
||||
merchantStaffUserId, fakeId, "MOCK-TEST-" + fakeId, "【测试】食堂", "【测试】收货地址");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@Inner
|
||||
@PostMapping("/inner/sync-from-order")
|
||||
@Operation(summary = "订单支付成功回调(内部服务)",
|
||||
description = "由订单服务 Feign 调用:落库、按单位默认骑手池指派骑手;指派成功且事务提交后,经 Redis 向骑手 WebSocket 下发新单通知(见食堂文档「骑手推送与联调规范」)。未指派(无池)则不推送。")
|
||||
description = "由订单服务 Feign 调用:落库、按单位默认骑手池指派骑手;事务提交后:立即送经 Redis 向商家端 WS 发新单通知(bizType=MERCHANT_NEW_ORDER);指派成功时再向骑手 WS 发新单(见「骑手推送与联调规范」)。未指派(无池)则不推骑手。预约单商家提醒仍由定时任务或当场补跑 notifyMerchantIfDue。")
|
||||
public R<Long> syncFromOrder(@Valid @RequestBody CanteenDeliveryOrderSyncDTO dto) {
|
||||
return canteenDeliveryOrderService.syncFromOrder(dto);
|
||||
}
|
||||
@@ -85,6 +114,23 @@ public class CanteenDeliveryOrderController {
|
||||
return R.ok(vo);
|
||||
}
|
||||
|
||||
@GetMapping("/app/mch/list")
|
||||
@Operation(summary = "商家端 App-外送单列表",
|
||||
description = "当前登录用户部门及其下级部门关联的商户(钱包 get-by-dept-ids)下的外送单。"
|
||||
+ "tab:pending 进行中 | all全部 | completed 已完成 | cancelled 已取消。分页同骑手端。"
|
||||
+ "预约单 SCHEDULED 仅在 merchant_notify_done=1(已向商家触发通知)后返回;立即送不受限。")
|
||||
public R<Page<CanteenDeliveryOrderAppVO>> mchAppList(@RequestParam(defaultValue = "pending") String tab,
|
||||
@ParameterObject Page<CanteenDeliveryOrderEntity> page) {
|
||||
return canteenDeliveryOrderService.pageForMerchantApp(tab, page);
|
||||
}
|
||||
|
||||
@GetMapping("/app/mch/{id}")
|
||||
@Operation(summary = "商家端 App-外送单详情",
|
||||
description = "预约单未到商家通知完成前返回失败(与列表可见性一致)。")
|
||||
public R<CanteenDeliveryOrderAppVO> mchAppDetail(@PathVariable Long id) {
|
||||
return canteenDeliveryOrderService.detailForMerchantApp(id);
|
||||
}
|
||||
|
||||
@SysLog("取餐拍照")
|
||||
@PostMapping("/app/pickup")
|
||||
@Operation(summary = "骑手端-到店取餐拍照",
|
||||
|
||||
+251
-5
@@ -1,27 +1,273 @@
|
||||
package com.bsd.pjcloud.canteen.push;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.RemoteUserService;
|
||||
import com.bsd.pjcloud.canteen.constants.CanteenDeliveryPushConstants;
|
||||
import com.bsd.pjcloud.canteen.entity.CanteenDeliveryOrderEntity;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 预约外送单到达「通知商家」时间点时的扩展点:当前仅打日志,后续可接 MQ、站内信、商户端 WebSocket 等。
|
||||
* 商家端 WebSocket:与骑手相同链路(Redis {@code websocket-send})。接收人为商户部门链上的用户(与 {@code pageForMerchantApp} 一致)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CanteenDeliveryMerchantNotifyPublisher {
|
||||
|
||||
private final RemoteMchService remoteMchService;
|
||||
private final RemoteDeptService remoteDeptService;
|
||||
private final RemoteUserService remoteUserService;
|
||||
private final ObjectProvider<StringRedisTemplate> stringRedisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 联调:预约提醒结构(与定时任务一致)。
|
||||
*/
|
||||
public void publishScheduledRemindMock(Long merchantStaffUserId,
|
||||
Long deliveryOrderId,
|
||||
String sourceOrderId,
|
||||
String merchantName,
|
||||
String fullAddress) {
|
||||
if (merchantStaffUserId == null) {
|
||||
return;
|
||||
}
|
||||
sendEnvelope(
|
||||
List.of(merchantStaffUserId),
|
||||
deliveryOrderId,
|
||||
sourceOrderId,
|
||||
merchantName,
|
||||
fullAddress,
|
||||
"SCHEDULED",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CanteenDeliveryPushConstants.BIZ_TYPE_SCHEDULED_MERCHANT_REMIND,
|
||||
"预约外送提醒",
|
||||
"您有一单预约外送即将进入备餐时段,请及时查看");
|
||||
}
|
||||
|
||||
/**
|
||||
* 联调:新单通知结构(与立即送支付同步后一致)。
|
||||
*/
|
||||
public void publishMerchantNewOrderMock(Long merchantStaffUserId,
|
||||
Long deliveryOrderId,
|
||||
String sourceOrderId,
|
||||
String merchantName,
|
||||
String fullAddress) {
|
||||
if (merchantStaffUserId == null) {
|
||||
return;
|
||||
}
|
||||
sendEnvelope(
|
||||
List.of(merchantStaffUserId),
|
||||
deliveryOrderId,
|
||||
sourceOrderId,
|
||||
merchantName,
|
||||
fullAddress,
|
||||
"IMMEDIATE",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CanteenDeliveryPushConstants.BIZ_TYPE_MERCHANT_NEW_ORDER,
|
||||
"新外送订单",
|
||||
"您有一单新的食堂外送订单,请及时处理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即送:支付同步落库后通知商家。
|
||||
*/
|
||||
public void publishImmediateNewOrderNotice(CanteenDeliveryOrderEntity order) {
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> userIds = resolveMerchantStaffUserIds(order);
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sendEnvelope(
|
||||
userIds,
|
||||
order.getId(),
|
||||
order.getSourceOrderId(),
|
||||
order.getMerchantName(),
|
||||
order.getFullAddress(),
|
||||
order.getExpectDeliveryType(),
|
||||
order.getExpectDeliveryDate(),
|
||||
order.getExpectDeliveryStart(),
|
||||
order.getExpectDeliveryEnd(),
|
||||
order.getExpectArriveTime(),
|
||||
CanteenDeliveryPushConstants.BIZ_TYPE_MERCHANT_NEW_ORDER,
|
||||
"新外送订单",
|
||||
"您有一单新的食堂外送订单,请及时处理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约单到达「通知商家」时间点(定时任务触发)。
|
||||
*/
|
||||
public void publishScheduledOrderNotice(CanteenDeliveryOrderEntity order) {
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
log.info(
|
||||
"预约外送-商家提醒(占位实现,可扩展): deliveryOrderId={} merchantId={} sourceOrderId={} expectDate={} expectStart={} expectEnd={}",
|
||||
List<Long> userIds = resolveMerchantStaffUserIds(order);
|
||||
if (userIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sendEnvelope(
|
||||
userIds,
|
||||
order.getId(),
|
||||
order.getMerchantId(),
|
||||
order.getSourceOrderId(),
|
||||
order.getMerchantName(),
|
||||
order.getFullAddress(),
|
||||
order.getExpectDeliveryType(),
|
||||
order.getExpectDeliveryDate(),
|
||||
order.getExpectDeliveryStart(),
|
||||
order.getExpectDeliveryEnd());
|
||||
order.getExpectDeliveryEnd(),
|
||||
order.getExpectArriveTime(),
|
||||
CanteenDeliveryPushConstants.BIZ_TYPE_SCHEDULED_MERCHANT_REMIND,
|
||||
"预约外送提醒",
|
||||
"您有一单预约外送即将进入备餐时段,请及时查看");
|
||||
}
|
||||
|
||||
private List<Long> resolveMerchantStaffUserIds(CanteenDeliveryOrderEntity order) {
|
||||
if (StrUtil.isBlank(order.getMerchantId())) {
|
||||
log.warn("商家 WS 跳过: deliveryOrderId={} 无 merchantId", order.getId());
|
||||
return List.of();
|
||||
}
|
||||
long mchPk;
|
||||
try {
|
||||
mchPk = Long.parseLong(order.getMerchantId().trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
log.warn("商家 WS 跳过: deliveryOrderId={} merchantId 非数字 {}", order.getId(), order.getMerchantId());
|
||||
return List.of();
|
||||
}
|
||||
R<MchManageVo> mchR = remoteMchService.getById(mchPk);
|
||||
if (mchR == null || !mchR.isOk() || mchR.getData() == null || mchR.getData().getDeptId() == null) {
|
||||
log.warn("商家 WS 跳过: deliveryOrderId={} 无法解析商户 dept, mchId={}", order.getId(), mchPk);
|
||||
return List.of();
|
||||
}
|
||||
Long mchDeptId = mchR.getData().getDeptId();
|
||||
List<Long> deptIds = new ArrayList<>();
|
||||
R<List<SysDept>> chainR = remoteDeptService.getParentDeptList(mchDeptId);
|
||||
if (chainR != null && chainR.isOk() && CollUtil.isNotEmpty(chainR.getData())) {
|
||||
for (SysDept d : chainR.getData()) {
|
||||
if (d != null && d.getDeptId() != null) {
|
||||
deptIds.add(d.getDeptId());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deptIds.isEmpty()) {
|
||||
deptIds.add(mchDeptId);
|
||||
}
|
||||
R<List<SysUser>> usersR = remoteUserService.getUserIdListByDeptIdList(deptIds);
|
||||
if (usersR == null || !usersR.isOk() || CollUtil.isEmpty(usersR.getData())) {
|
||||
log.info("商家 WS 无接收用户: deliveryOrderId={} mchDeptId={} deptIds={}", order.getId(), mchDeptId, deptIds);
|
||||
return List.of();
|
||||
}
|
||||
Set<Long> userIds = usersR.getData().stream()
|
||||
.map(SysUser::getUserId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (userIds.isEmpty()) {
|
||||
log.info("商家 WS 无有效 userId: deliveryOrderId={}", order.getId());
|
||||
return List.of();
|
||||
}
|
||||
return userIds.stream().toList();
|
||||
}
|
||||
|
||||
private void sendEnvelope(List<Long> userIds,
|
||||
Long deliveryOrderId,
|
||||
String sourceOrderId,
|
||||
String merchantName,
|
||||
String fullAddress,
|
||||
String expectDeliveryType,
|
||||
java.time.LocalDate expectDeliveryDate,
|
||||
java.time.LocalTime expectDeliveryStart,
|
||||
java.time.LocalTime expectDeliveryEnd,
|
||||
LocalDateTime expectArriveTime,
|
||||
String bizType,
|
||||
String title,
|
||||
String body) {
|
||||
if (CollUtil.isEmpty(userIds) || deliveryOrderId == null) {
|
||||
return;
|
||||
}
|
||||
StringRedisTemplate redis = stringRedisTemplate.getIfAvailable();
|
||||
if (redis == null) {
|
||||
log.debug("StringRedisTemplate 不可用,跳过商家端 WS 推送");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("deliveryOrderId", deliveryOrderId);
|
||||
payload.put("sourceOrderId", sourceOrderId != null ? sourceOrderId : "");
|
||||
payload.put("merchantName", merchantName != null ? merchantName : "");
|
||||
payload.put("fullAddress", fullAddress != null ? fullAddress : "");
|
||||
payload.put("expectDeliveryType", expectDeliveryType != null ? expectDeliveryType : "");
|
||||
if (expectDeliveryDate != null) {
|
||||
payload.put("expectDeliveryDate", expectDeliveryDate.toString());
|
||||
} else {
|
||||
payload.putNull("expectDeliveryDate");
|
||||
}
|
||||
if (expectDeliveryStart != null) {
|
||||
payload.put("expectDeliveryStart", expectDeliveryStart.toString());
|
||||
} else {
|
||||
payload.putNull("expectDeliveryStart");
|
||||
}
|
||||
if (expectDeliveryEnd != null) {
|
||||
payload.put("expectDeliveryEnd", expectDeliveryEnd.toString());
|
||||
} else {
|
||||
payload.putNull("expectDeliveryEnd");
|
||||
}
|
||||
if (expectArriveTime != null) {
|
||||
payload.put("expectArriveTime", expectArriveTime.toString());
|
||||
} else {
|
||||
payload.putNull("expectArriveTime");
|
||||
}
|
||||
|
||||
ObjectNode envelope = objectMapper.createObjectNode();
|
||||
envelope.put("bizType", bizType);
|
||||
envelope.put("version", CanteenDeliveryPushConstants.PAYLOAD_VERSION);
|
||||
envelope.put("title", title);
|
||||
envelope.put("body", body);
|
||||
envelope.set("payload", payload);
|
||||
ObjectNode route = objectMapper.createObjectNode();
|
||||
route.put("type", "navigate");
|
||||
route.put("path", "/pages/canteen/mch/delivery-detail");
|
||||
route.set("query", objectMapper.valueToTree(Map.of("id", String.valueOf(deliveryOrderId))));
|
||||
envelope.set("route", route);
|
||||
|
||||
String messageText = objectMapper.writeValueAsString(envelope);
|
||||
List<String> sessionKeys = userIds.stream().map(String::valueOf).toList();
|
||||
Map<String, Object> messageDo = Map.of(
|
||||
"needBroadcast", false,
|
||||
"sessionKeys", sessionKeys,
|
||||
"messageText", messageText
|
||||
);
|
||||
redis.convertAndSend(CanteenDeliveryRiderPushPublisher.REDIS_CHANNEL_WEBSOCKET_SEND,
|
||||
objectMapper.writeValueAsString(messageDo));
|
||||
} catch (Exception e) {
|
||||
log.warn("商家 WS Redis 发布失败 deliveryOrderId={} bizType={}", deliveryOrderId, bizType, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -25,6 +25,16 @@ public interface CanteenDeliveryOrderService extends IService<CanteenDeliveryOrd
|
||||
|
||||
CanteenDeliveryOrderAppVO detailForRider(Long riderUserId, Long id);
|
||||
|
||||
/**
|
||||
* 商家端 App:按当前登录用户部门树解析商户(钱包 {@code /mch-manage/get-by-dept-ids}),仅返回这些商户的外送单。
|
||||
* 预约单({@code SCHEDULED})仅在已向商家触发通知({@code merchant_notify_done = 1})后返回;立即送不受此限。
|
||||
*
|
||||
* @param tab pending 进行中(待派单/预约待派/待取餐/配送中)| completed | cancelled | all
|
||||
*/
|
||||
R<Page<CanteenDeliveryOrderAppVO>> pageForMerchantApp(String tab, Page<CanteenDeliveryOrderEntity> page);
|
||||
|
||||
R<CanteenDeliveryOrderAppVO> detailForMerchantApp(Long id);
|
||||
|
||||
R<Void> pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto);
|
||||
|
||||
R<Void> deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto);
|
||||
|
||||
+225
-1
@@ -1,6 +1,8 @@
|
||||
package com.bsd.pjcloud.canteen.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -27,9 +29,14 @@ 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 org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import com.bsd.pjcloud.canteen.vo.app.CanteenDeliveryOrderAppVO;
|
||||
import com.bsd.pjcloud.common.core.util.BeanUtils;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateTakeOutStatusDto;
|
||||
import com.bsd.pjcloud.hard.api.feign.RemoteHardOrderService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -45,7 +52,10 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -63,6 +73,14 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
|
||||
private final CanteenDeliveryMerchantNotifyPublisher merchantNotifyPublisher;
|
||||
private final RemoteParamService remoteParamService;
|
||||
private final RemoteHardOrderService remoteHardOrderService;
|
||||
private final RemoteMchService remoteMchService;
|
||||
|
||||
private CanteenDeliveryOrderService deliveryOrderServiceSelf;
|
||||
|
||||
@Autowired
|
||||
public void setDeliveryOrderServiceSelf(@Lazy CanteenDeliveryOrderService deliveryOrderServiceSelf) {
|
||||
this.deliveryOrderServiceSelf = deliveryOrderServiceSelf;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -122,6 +140,7 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
|
||||
entity.setScheduledRiderNotifyTime(null);
|
||||
assignRiderOnCreate(entity);
|
||||
save(entity);
|
||||
scheduleMerchantImmediatePushAfterCommit(entity);
|
||||
scheduleRiderPushAfterAssign(entity);
|
||||
return R.ok(entity.getId());
|
||||
}
|
||||
@@ -146,10 +165,49 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
|
||||
|
||||
assignRiderOnCreate(entity);
|
||||
save(entity);
|
||||
scheduleNotifyMerchantIfDueAfterCommit(entity.getId());
|
||||
scheduleRiderPushAfterAssign(entity);
|
||||
return R.ok(entity.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即送:事务提交后通知商家有新单(与骑手推送一样延后到 afterCommit)。
|
||||
*/
|
||||
private void scheduleMerchantImmediatePushAfterCommit(CanteenDeliveryOrderEntity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
merchantNotifyPublisher.publishImmediateNewOrderNotice(entity);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
merchantNotifyPublisher.publishImmediateNewOrderNotice(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约单当场指派骑手时:若已到「通知商家」时间,补跑幂等通知(避免只依赖定时任务延迟)。
|
||||
*/
|
||||
private void scheduleNotifyMerchantIfDueAfterCommit(Long deliveryOrderId) {
|
||||
if (deliveryOrderId == null || deliveryOrderServiceSelf == null) {
|
||||
return;
|
||||
}
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
deliveryOrderServiceSelf.notifyMerchantIfDue(deliveryOrderId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deliveryOrderServiceSelf.notifyMerchantIfDue(deliveryOrderId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务提交后再推送,避免回滚后骑手仍收到通知。
|
||||
*/
|
||||
@@ -346,9 +404,156 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
|
||||
return toAppVoWithCart(e);
|
||||
}
|
||||
|
||||
private R<List<String>> resolveMerchantIdStringsForLoginUser() {
|
||||
var user = SecurityUtils.getUser();
|
||||
if (user == null || user.getId() == null) {
|
||||
return R.failed("未登录");
|
||||
}
|
||||
R<UserInfo> userInfoR = remoteUserService.getInnerByUserId(user.getId());
|
||||
if (userInfoR == null || !userInfoR.isOk() || userInfoR.getData() == null
|
||||
|| userInfoR.getData().getSysUser() == null) {
|
||||
return R.failed("用户信息不存在");
|
||||
}
|
||||
Long rootDeptId = userInfoR.getData().getSysUser().getDeptId();
|
||||
if (rootDeptId == null) {
|
||||
return R.failed("用户部门不存在");
|
||||
}
|
||||
List<Long> deptIds;
|
||||
R<List<SysDept>> deptR = remoteDeptService.getDescendantList(rootDeptId);
|
||||
if (deptR != null && deptR.isOk() && CollUtil.isNotEmpty(deptR.getData())) {
|
||||
deptIds = deptR.getData().stream().map(SysDept::getDeptId).distinct().toList();
|
||||
} else {
|
||||
deptIds = List.of(rootDeptId);
|
||||
}
|
||||
R<List<MchManageVo>> mchR = remoteMchService.getByDeptIds(deptIds);
|
||||
if (mchR == null || !mchR.isOk() || CollUtil.isEmpty(mchR.getData())) {
|
||||
String msg = mchR != null && StrUtil.isNotBlank(mchR.getMsg()) ? mchR.getMsg() : "商户不存在";
|
||||
return R.failed(msg);
|
||||
}
|
||||
List<String> ids = mchR.getData().stream()
|
||||
.map(MchManageVo::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::valueOf)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
return R.failed("商户不存在");
|
||||
}
|
||||
return R.ok(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约单仅当已向商家触发通知({@code merchant_notify_done = 1})后对商家端列表/详情可见,避免未到提醒时间即出现。
|
||||
*/
|
||||
private void applyMerchantScheduledVisibilityFilter(LambdaQueryWrapper<CanteenDeliveryOrderEntity> q) {
|
||||
q.and(w -> w.and(wInner -> wInner.isNull(CanteenDeliveryOrderEntity::getExpectDeliveryType)
|
||||
.or()
|
||||
.ne(CanteenDeliveryOrderEntity::getExpectDeliveryType, CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED))
|
||||
.or(w2 -> w2.eq(CanteenDeliveryOrderEntity::getExpectDeliveryType, CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED)
|
||||
.eq(CanteenDeliveryOrderEntity::getMerchantNotifyDone, 1)));
|
||||
}
|
||||
|
||||
private boolean merchantScheduledOrderVisibleToMerchant(CanteenDeliveryOrderEntity e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
if (!CanteenDeliveryScheduleConstants.EXPECT_TYPE_SCHEDULED.equalsIgnoreCase(
|
||||
StrUtil.trim(StrUtil.emptyToDefault(e.getExpectDeliveryType(), "")))) {
|
||||
return true;
|
||||
}
|
||||
Integer done = e.getMerchantNotifyDone();
|
||||
return done != null && done == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Page<CanteenDeliveryOrderAppVO>> pageForMerchantApp(String tab, Page<CanteenDeliveryOrderEntity> page) {
|
||||
R<List<String>> mchIdsR = resolveMerchantIdStringsForLoginUser();
|
||||
if (!mchIdsR.isOk()) {
|
||||
return R.failed(mchIdsR.getMsg());
|
||||
}
|
||||
List<String> merchantIds = mchIdsR.getData();
|
||||
Page<CanteenDeliveryOrderEntity> p = page != null ? page : new Page<>(1, 10);
|
||||
if (p.getCurrent() < 1) {
|
||||
p.setCurrent(1);
|
||||
}
|
||||
if (p.getSize() < 1) {
|
||||
p.setSize(10);
|
||||
} else if (p.getSize() > RIDER_LIST_MAX_PAGE_SIZE) {
|
||||
p.setSize(RIDER_LIST_MAX_PAGE_SIZE);
|
||||
}
|
||||
String t = StrUtil.emptyToDefault(tab, "all");
|
||||
var q = Wrappers.<CanteenDeliveryOrderEntity>lambdaQuery()
|
||||
.in(CanteenDeliveryOrderEntity::getMerchantId, merchantIds);
|
||||
applyMerchantScheduledVisibilityFilter(q);
|
||||
switch (t) {
|
||||
case "pending" -> q.in(CanteenDeliveryOrderEntity::getStatus,
|
||||
CanteenDeliveryOrderStatus.PENDING_ASSIGN,
|
||||
CanteenDeliveryOrderStatus.SCHEDULED_PENDING,
|
||||
CanteenDeliveryOrderStatus.WAIT_PICKUP,
|
||||
CanteenDeliveryOrderStatus.DELIVERING);
|
||||
case "completed" -> q.eq(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.COMPLETED);
|
||||
case "cancelled" -> q.eq(CanteenDeliveryOrderEntity::getStatus, CanteenDeliveryOrderStatus.CANCELLED);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
q.orderByDesc(CanteenDeliveryOrderEntity::getCreateTime);
|
||||
Page<CanteenDeliveryOrderEntity> entityPage = page(p, q);
|
||||
Page<CanteenDeliveryOrderAppVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||
Map<Long, SysUser> riderById = loadRidersByIds(entityPage.getRecords().stream()
|
||||
.map(CanteenDeliveryOrderEntity::getRiderUserId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList());
|
||||
voPage.setRecords(entityPage.getRecords().stream().map(e -> toAppVoWithCart(e, riderById)).toList());
|
||||
return R.ok(voPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<CanteenDeliveryOrderAppVO> detailForMerchantApp(Long id) {
|
||||
R<List<String>> mchIdsR = resolveMerchantIdStringsForLoginUser();
|
||||
if (!mchIdsR.isOk()) {
|
||||
return R.failed(mchIdsR.getMsg());
|
||||
}
|
||||
List<String> merchantIds = mchIdsR.getData();
|
||||
CanteenDeliveryOrderEntity e = getById(id);
|
||||
if (e == null || StrUtil.isBlank(e.getMerchantId()) || !merchantIds.contains(e.getMerchantId())) {
|
||||
return R.failed("订单不存在或无权查看");
|
||||
}
|
||||
if (!merchantScheduledOrderVisibleToMerchant(e)) {
|
||||
return R.failed("订单不存在或无权查看");
|
||||
}
|
||||
Map<Long, SysUser> riderById = loadRidersByIds(
|
||||
e.getRiderUserId() != null ? List.of(e.getRiderUserId()) : List.of());
|
||||
return R.ok(toAppVoWithCart(e, riderById));
|
||||
}
|
||||
|
||||
private Map<Long, SysUser> loadRidersByIds(List<Long> riderUserIds) {
|
||||
if (CollUtil.isEmpty(riderUserIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
R<List<SysUser>> r = remoteUserService.getInnerUserById(riderUserIds);
|
||||
if (r == null || !r.isOk() || CollUtil.isEmpty(r.getData())) {
|
||||
return Map.of();
|
||||
}
|
||||
return r.getData().stream()
|
||||
.filter(u -> u.getUserId() != null)
|
||||
.collect(Collectors.toMap(SysUser::getUserId, Function.identity(), (a, b) -> a));
|
||||
}
|
||||
|
||||
private CanteenDeliveryOrderAppVO toAppVoWithCart(CanteenDeliveryOrderEntity e) {
|
||||
return toAppVoWithCart(e, null);
|
||||
}
|
||||
|
||||
private CanteenDeliveryOrderAppVO toAppVoWithCart(CanteenDeliveryOrderEntity e, Map<Long, SysUser> riderById) {
|
||||
CanteenDeliveryOrderAppVO vo = BeanUtils.toBean(e, CanteenDeliveryOrderAppVO.class);
|
||||
vo.setCarList(parseCartList(e.getCarListJson()));
|
||||
if (riderById != null && e.getRiderUserId() != null) {
|
||||
SysUser rider = riderById.get(e.getRiderUserId());
|
||||
if (rider != null) {
|
||||
vo.setRiderName(rider.getName());
|
||||
vo.setRiderMobile(rider.getPhone());
|
||||
}
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -455,9 +660,28 @@ public class CanteenDeliveryOrderServiceImpl extends ServiceImpl<CanteenDelivery
|
||||
if (e.getScheduledMerchantNotifyTime() == null || e.getScheduledMerchantNotifyTime().isAfter(LocalDateTime.now())) {
|
||||
return;
|
||||
}
|
||||
merchantNotifyPublisher.publishScheduledOrderNotice(e);
|
||||
e.setMerchantNotifyDone(1);
|
||||
updateById(e);
|
||||
scheduleMerchantPushAfterNotify(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务提交后再推商家 WS,避免回滚后仍收到通知(与骑手指派推送一致)。
|
||||
*/
|
||||
private void scheduleMerchantPushAfterNotify(CanteenDeliveryOrderEntity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
merchantNotifyPublisher.publishScheduledOrderNotice(entity);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
merchantNotifyPublisher.publishScheduledOrderNotice(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+38
-4
@@ -18,6 +18,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -127,15 +129,17 @@ class CanteenDeliveryOrderControllerTest {
|
||||
CanteenDeliveryOrderAppVO vo = new CanteenDeliveryOrderAppVO();
|
||||
vo.setId(1L);
|
||||
vo.setSourceOrderId("SO-1");
|
||||
when(canteenDeliveryOrderService.listForRider(RIDER_USER_ID, "pending")).thenReturn(List.of(vo));
|
||||
Page<CanteenDeliveryOrderAppVO> page = new Page<>(1, 10, 1);
|
||||
page.setRecords(List.of(vo));
|
||||
when(canteenDeliveryOrderService.pageForRider(eq(RIDER_USER_ID), eq("pending"), any(Page.class))).thenReturn(page);
|
||||
|
||||
mockMvc.perform(get("/canteenDeliveryOrder/app/list").param("tab", "pending"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.ok").value(true))
|
||||
.andExpect(jsonPath("$.data[0].id").value(1))
|
||||
.andExpect(jsonPath("$.data[0].sourceOrderId").value("SO-1"));
|
||||
.andExpect(jsonPath("$.data.records[0].id").value(1))
|
||||
.andExpect(jsonPath("$.data.records[0].sourceOrderId").value("SO-1"));
|
||||
|
||||
verify(canteenDeliveryOrderService).listForRider(RIDER_USER_ID, "pending");
|
||||
verify(canteenDeliveryOrderService).pageForRider(eq(RIDER_USER_ID), eq("pending"), any(Page.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,6 +163,36 @@ class CanteenDeliveryOrderControllerTest {
|
||||
.andExpect(jsonPath("$.ok").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mchAppList_delegatesToService() throws Exception {
|
||||
CanteenDeliveryOrderAppVO vo = new CanteenDeliveryOrderAppVO();
|
||||
vo.setId(2L);
|
||||
vo.setSourceOrderId("SO-MCH");
|
||||
Page<CanteenDeliveryOrderAppVO> page = new Page<>(1, 10, 1);
|
||||
page.setRecords(List.of(vo));
|
||||
when(canteenDeliveryOrderService.pageForMerchantApp(eq("pending"), any(Page.class))).thenReturn(R.ok(page));
|
||||
|
||||
mockMvc.perform(get("/canteenDeliveryOrder/app/mch/list").param("tab", "pending"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.ok").value(true))
|
||||
.andExpect(jsonPath("$.data.records[0].id").value(2))
|
||||
.andExpect(jsonPath("$.data.records[0].sourceOrderId").value("SO-MCH"));
|
||||
|
||||
verify(canteenDeliveryOrderService).pageForMerchantApp(eq("pending"), any(Page.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mchAppDetail_ok() throws Exception {
|
||||
CanteenDeliveryOrderAppVO vo = new CanteenDeliveryOrderAppVO();
|
||||
vo.setId(88L);
|
||||
when(canteenDeliveryOrderService.detailForMerchantApp(88L)).thenReturn(R.ok(vo));
|
||||
|
||||
mockMvc.perform(get("/canteenDeliveryOrder/app/mch/88"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.ok").value(true))
|
||||
.andExpect(jsonPath("$.data.id").value(88));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pickup_ok() throws Exception {
|
||||
when(canteenDeliveryOrderService.pickup(eq(RIDER_USER_ID), any())).thenReturn(R.ok());
|
||||
|
||||
Reference in new issue
Block a user