From 371c012498221fe821ee5e75f0b991ddb45fbfdc Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Wed, 15 Apr 2026 17:22:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=AE=B6=E5=A4=96=E5=8D=96=E8=AE=A2?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CanteenDeliveryPushConstants.java | 12 +- .../vo/app/CanteenDeliveryOrderAppVO.java | 11 +- .../CanteenDeliveryOrderController.java | 48 +++- ...anteenDeliveryMerchantNotifyPublisher.java | 256 +++++++++++++++++- .../service/CanteenDeliveryOrderService.java | 10 + .../impl/CanteenDeliveryOrderServiceImpl.java | 226 +++++++++++++++- .../CanteenDeliveryOrderControllerTest.java | 42 ++- 7 files changed, 592 insertions(+), 13 deletions(-) diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java index aae230889..20fcc5122 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/constants/CanteenDeliveryPushConstants.java @@ -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; } diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java index dd5c743c6..380434f01 100644 --- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java +++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/CanteenDeliveryOrderAppVO.java @@ -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; diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java index 6cbcae4fb..ebe05c3a0 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDeliveryOrderController.java @@ -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 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 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 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> mchAppList(@RequestParam(defaultValue = "pending") String tab, + @ParameterObject Page page) { + return canteenDeliveryOrderService.pageForMerchantApp(tab, page); + } + + @GetMapping("/app/mch/{id}") + @Operation(summary = "商家端 App-外送单详情", + description = "预约单未到商家通知完成前返回失败(与列表可见性一致)。") + public R mchAppDetail(@PathVariable Long id) { + return canteenDeliveryOrderService.detailForMerchantApp(id); + } + @SysLog("取餐拍照") @PostMapping("/app/pickup") @Operation(summary = "骑手端-到店取餐拍照", diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryMerchantNotifyPublisher.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryMerchantNotifyPublisher.java index 0cb72a961..0bc1c130a 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryMerchantNotifyPublisher.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/push/CanteenDeliveryMerchantNotifyPublisher.java @@ -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; + 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 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 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 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 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 deptIds = new ArrayList<>(); + R> 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> 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 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 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 sessionKeys = userIds.stream().map(String::valueOf).toList(); + Map 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); + } } } diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java index 7f7788184..352c54026 100644 --- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java +++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDeliveryOrderService.java @@ -25,6 +25,16 @@ public interface CanteenDeliveryOrderService extends IService> pageForMerchantApp(String tab, Page page); + + R detailForMerchantApp(Long id); + R pickup(Long riderUserId, CanteenDeliveryPhotoDTO dto); R deliver(Long riderUserId, CanteenDeliveryPhotoDTO dto); 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 6a0951555..06cf870e0 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 @@ -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> resolveMerchantIdStringsForLoginUser() { + var user = SecurityUtils.getUser(); + if (user == null || user.getId() == null) { + return R.failed("未登录"); + } + R 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 deptIds; + R> 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> 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 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 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> pageForMerchantApp(String tab, Page page) { + R> mchIdsR = resolveMerchantIdStringsForLoginUser(); + if (!mchIdsR.isOk()) { + return R.failed(mchIdsR.getMsg()); + } + List merchantIds = mchIdsR.getData(); + Page 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.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 entityPage = page(p, q); + Page voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal()); + Map 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 detailForMerchantApp(Long id) { + R> mchIdsR = resolveMerchantIdStringsForLoginUser(); + if (!mchIdsR.isOk()) { + return R.failed(mchIdsR.getMsg()); + } + List 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 riderById = loadRidersByIds( + e.getRiderUserId() != null ? List.of(e.getRiderUserId()) : List.of()); + return R.ok(toAppVoWithCart(e, riderById)); + } + + private Map loadRidersByIds(List riderUserIds) { + if (CollUtil.isEmpty(riderUserIds)) { + return Map.of(); + } + R> 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 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 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 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());