diff --git a/hiver-admin/pom.xml b/hiver-admin/pom.xml index 9874cf8a..d4991a66 100644 --- a/hiver-admin/pom.xml +++ b/hiver-admin/pom.xml @@ -71,6 +71,11 @@ org.springframework.boot spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-amqp + diff --git a/hiver-admin/src/main/java/cc/hiver/config/RabbitMqConfig.java b/hiver-admin/src/main/java/cc/hiver/config/RabbitMqConfig.java new file mode 100644 index 00000000..413afcda --- /dev/null +++ b/hiver-admin/src/main/java/cc/hiver/config/RabbitMqConfig.java @@ -0,0 +1,37 @@ +package cc.hiver.config; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * RabbitMQ 配置类 + */ +@Configuration +public class RabbitMqConfig { + + public static final String EXCHANGE_NAME = "hiver.direct.exchange"; + public static final String QUEUE_NAME = "hiver.demo.queue"; + public static final String ROUTING_KEY = "hiver.demo.routing.key"; + + // 1. 定义Direct交换机 + @Bean + public DirectExchange directExchange() { + return new DirectExchange(EXCHANGE_NAME, true, false); + } + + // 2. 定义队列 (durable=true 持久化) + @Bean + public Queue demoQueue() { + return new Queue(QUEUE_NAME, true); + } + + // 3. 将队列绑定到交换机 + @Bean + public Binding bindingDemoQueue() { + return BindingBuilder.bind(demoQueue()).to(directExchange()).with(ROUTING_KEY); + } +} diff --git a/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqConsumer.java b/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqConsumer.java new file mode 100644 index 00000000..7d6247f3 --- /dev/null +++ b/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqConsumer.java @@ -0,0 +1,24 @@ +package cc.hiver.rabbitmq; + +import cc.hiver.config.RabbitMqConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +/** + * RabbitMQ 消费者示例 + */ +@Slf4j +@Component +public class RabbitMqConsumer { + + /** + * 监听指定的队列 + * @param message 消息内容 + */ + @RabbitListener(queues = RabbitMqConfig.QUEUE_NAME) + public void receiveMessage(String message) { + log.info("【RabbitMQ消费消息】收到来自队列 {} 的消息: {}", RabbitMqConfig.QUEUE_NAME, message); + // 此处编写业务处理逻辑 + } +} diff --git a/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqProducer.java b/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqProducer.java new file mode 100644 index 00000000..0c271ed9 --- /dev/null +++ b/hiver-admin/src/main/java/cc/hiver/rabbitmq/RabbitMqProducer.java @@ -0,0 +1,28 @@ +package cc.hiver.rabbitmq; + +import cc.hiver.config.RabbitMqConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * RabbitMQ 生产者示例 + */ +@Slf4j +@Component +public class RabbitMqProducer { + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * 发送消息 + * @param message 消息内容 + */ + public void sendMessage(String message) { + log.info("【RabbitMQ生产消息】试图发送消息: {}", message); + rabbitTemplate.convertAndSend(RabbitMqConfig.EXCHANGE_NAME, RabbitMqConfig.ROUTING_KEY, message); + log.info("【RabbitMQ生产消息】发送成功"); + } +} diff --git a/hiver-admin/src/main/resources/application.yml b/hiver-admin/src/main/resources/application.yml index 8bd133cd..dfcde390 100644 --- a/hiver-admin/src/main/resources/application.yml +++ b/hiver-admin/src/main/resources/application.yml @@ -85,6 +85,27 @@ spring: rest: # 要连接的ES客户端Rest Uri 多个逗号分隔 uris: http://localhost:9200 + # RabbitMQ配置 + rabbitmq: + host: 8.140.253.224 + port: 5672 + username: root + password: ziyi123QQ + virtual-host: / + # 连接超时,单位毫秒 + connection-timeout: 15S + listener: + simple: + # 手动确认消息 + acknowledge-mode: manual + # 开启重试 + retry: + enabled: true + max-attempts: 3 + template: + retry: + enabled: true + max-attempts: 3 # 定时任务 quartz: # 任务信息存储至数据库 diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 329887da..5b9d2be8 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • - 22, 2026 16:23:04 + 18, 2026 14:58:33
  • @@ -84,7 +84,7 @@

    passTest

    -

    16:23:04 / 0.019 secs

    +

    14:58:34 / 0.017 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -03.22.2026 16:23:04 -03.22.2026 16:23:04 -0.019 secs +04.18.2026 14:58:34 +04.18.2026 14:58:34 +0.017 secs
    @@ -104,7 +104,7 @@ Pass - 16:23:04 + 14:58:34 Test passed @@ -128,13 +128,13 @@

    Started

    -

    22, 2026 16:23:04

    +

    18, 2026 14:58:33

    Ended

    -

    22, 2026 16:23:04

    +

    18, 2026 14:58:34

    diff --git a/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java b/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java index 238c0522..c2a68111 100644 --- a/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java +++ b/hiver-core/src/main/java/cc/hiver/core/common/constant/SettingConstant.java @@ -98,7 +98,13 @@ public interface SettingConstant { // 重置密码 SMS_RESET_PASS, // 工作流消息 - SMS_ACTIVITI + SMS_ACTIVITI, + // 无人接单用户通知 + SMS_NO_ORDER, + // 拒绝接单通知 + SMS_REJECT_ORDER, + // 被指定未处理通知 + SMS_TIMEOUT } /** diff --git a/hiver-core/src/main/java/cc/hiver/core/service/JPushService.java b/hiver-core/src/main/java/cc/hiver/core/service/JPushService.java index e2d89d66..9dbbefdc 100644 --- a/hiver-core/src/main/java/cc/hiver/core/service/JPushService.java +++ b/hiver-core/src/main/java/cc/hiver/core/service/JPushService.java @@ -2,6 +2,6 @@ package cc.hiver.core.service; public interface JPushService { - void sendPushNotification(String registrationId, String message); + void sendPushNotification(String registrationId, String message,String orderId); } diff --git a/hiver-core/src/main/java/cc/hiver/core/serviceimpl/JPushServiceImpl.java b/hiver-core/src/main/java/cc/hiver/core/serviceimpl/JPushServiceImpl.java index 28888cce..e93abfd7 100644 --- a/hiver-core/src/main/java/cc/hiver/core/serviceimpl/JPushServiceImpl.java +++ b/hiver-core/src/main/java/cc/hiver/core/serviceimpl/JPushServiceImpl.java @@ -22,7 +22,7 @@ public class JPushServiceImpl implements JPushService { @Override - public void sendPushNotification(String registrationId, String message) { + public void sendPushNotification(String registrationId, String message,String orderId) { PushPayload payload = PushPayload.newBuilder() .setPlatform(cn.jpush.api.push.model.Platform.all()) .setAudience(cn.jpush.api.push.model.audience.Audience.registrationId(registrationId)) @@ -31,7 +31,7 @@ public class JPushServiceImpl implements JPushService { .setMsgContent("消息内容") .addExtra("custom_key", "custom_value") // 额外参数 .addExtra("type", "订单通知") - .addExtra("order_id", "123456") + .addExtra("order_id",orderId) .addExtra("timestamp", System.currentTimeMillis()) .build()) .build(); diff --git a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java index eefed6d7..3974d7f6 100644 --- a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java +++ b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java @@ -20,9 +20,11 @@ import cc.hiver.core.vo.RoleDTO; import cc.hiver.core.vo.WechatVo; import cc.hiver.mall.entity.Shop; import cc.hiver.mall.entity.ShopArea; +import cc.hiver.mall.entity.ShopTakeaway; import cc.hiver.mall.entity.WorkerRelaPrice; import cc.hiver.mall.service.ShopAreaService; import cc.hiver.mall.service.ShopService; +import cc.hiver.mall.service.ShopTakeawayService; import cc.hiver.mall.service.ShopUserService; import cc.hiver.mall.service.mybatis.CustomerService; import cc.hiver.mall.service.mybatis.WorkerRelaPriceService; @@ -128,6 +130,10 @@ public class UserController { @Autowired private MemberSocialService memberSocialService; + @Autowired + private ShopTakeawayService shopTakeawayService; + + /** * 微信小程序登录凭证校验 @@ -668,22 +674,35 @@ public class UserController { List shop = shopService.getShopByUserid(wechatUser.getId()); if(shop != null){ + List shopIds = shop.stream().map(Shop::getId).collect(Collectors.toList()); + List shopTakeaway = shopTakeawayService.selectListByshopId(shopIds); + if(shopTakeaway != null){ + shop.forEach(e -> { + for(ShopTakeaway shopTakeawa : shopTakeaway) { + if (e.getId().equals(shopTakeawa.getShopId())) { + e.setShopTakeaway(shopTakeawa); + } + } + }); + } resultMap.put("shop", shop); } Worker worker = workerService.findByUserId(wechatUser.getId()); - if(worker.getGetPushOrder() == 1){ + if(worker!= null && worker.getGetPushOrder() == 1){ final List workerRelaPriceList = workerRelaPriceService.selectByWorkerId(worker.getWorkerId()); List workerRelaPriceListWaimai = new ArrayList<>(); List workerRelaPriceListKuaidi = new ArrayList<>(); - workerRelaPriceList.forEach(workerRelaPrice -> { - if(workerRelaPrice.getGetPushOrder() == 1){ - if(workerRelaPrice.getOrderType() == 0 ){ - workerRelaPriceListWaimai.add(workerRelaPrice); - }else{ - workerRelaPriceListKuaidi.add(workerRelaPrice); + if(workerRelaPriceList != null && workerRelaPriceList.size() > 0){ + workerRelaPriceList.forEach(workerRelaPrice -> { + if(workerRelaPrice.getGetPushOrder() == 1){ + if(workerRelaPrice.getOrderType() == 0 ){ + workerRelaPriceListWaimai.add(workerRelaPrice); + }else{ + workerRelaPriceListKuaidi.add(workerRelaPrice); + } } - } - }); + }); + } if(workerRelaPriceListWaimai.size() > 0){ resultMap.put("waimaiData", workerRelaPriceListWaimai); } diff --git a/hiver-modules/hiver-mall/pom.xml b/hiver-modules/hiver-mall/pom.xml index 0e00968e..09424a86 100644 --- a/hiver-modules/hiver-mall/pom.xml +++ b/hiver-modules/hiver-mall/pom.xml @@ -34,6 +34,15 @@ 1.0-SNAPSHOT compile + + org.springframework.boot + spring-boot-starter-amqp + + + com.alibaba + fastjson + 1.2.83 + \ No newline at end of file diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java index 66da5cd2..811a26cc 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java @@ -170,6 +170,9 @@ public class AdminSettlementController { private String settlementDate; } + @Autowired + private org.springframework.amqp.rabbit.core.RabbitTemplate rabbitTemplate; + @PostMapping("/confirmByShop") @ApiOperation(value = "每日结算确认(按商家)") public Result confirmByShop(@RequestBody ConfirmShopReq req) { @@ -177,28 +180,13 @@ public class AdminSettlementController { return ResultUtil.error("请选择要确认的商家"); } try { - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - if (StringUtils.isNotBlank(req.getRegionId())) { - qw.eq(MallSettlementRecord::getRegionId, req.getRegionId()); - } - if (StringUtils.isNotBlank(req.getSettlementDate())) { - qw.apply("DATE(create_time) = {0}", req.getSettlementDate()); - } - qw.in(MallSettlementRecord::getShopId, req.getShopIds()); - qw.eq(MallSettlementRecord::getStatus, 0); - - List list = mallSettlementRecordService.list(qw); - if (list == null || list.isEmpty()) { - return ResultUtil.success("没有需要确认的结算记录"); - } - List recordIds = list.stream().map(MallSettlementRecord::getId).collect(Collectors.toList()); - - mallSettlementRecordService.confirmSettlements(recordIds); - - return ResultUtil.success("结算确认成功"); + // 将结算参数转成JSON并投递给RabbitMQ + String message = com.alibaba.fastjson.JSON.toJSONString(req); + rabbitTemplate.convertAndSend(cc.hiver.mall.mq.SettlementMqConfig.SETTLEMENT_EXCHANGE, cc.hiver.mall.mq.SettlementMqConfig.SETTLEMENT_CONFIRM_ROUTING_KEY, message); + return ResultUtil.success("结算确认任务已投递,后台排队处理中"); } catch (Exception e) { - log.error("结算确认失败: {}", e.getMessage(), e); - return ResultUtil.error("结算确认失败: " + e.getMessage()); + log.error("推送结算确认MQ消息失败: {}", e.getMessage(), e); + return ResultUtil.error("推送任务失败: " + e.getMessage()); } } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java index cf559d18..db2b5262 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallDeliveryOrderController.java @@ -212,7 +212,6 @@ public class MallDeliveryOrderController { @RequestParam String workerId) { try { mallDeliveryOrderService.workerPickup(deliveryId, workerId); - jPushService.sendPushNotification("1507bfd3f6e2c0dbc0a", "您有一笔新的订单"); return ResultUtil.success("取货成功"); } catch (Exception e) { log.error("取货失败: {}", e.getMessage(), e); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/PushController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/PushController.java index 97ab3512..48fed97d 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/PushController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/PushController.java @@ -26,7 +26,7 @@ public class PushController { @GetMapping("/push") @ApiOperation(value = "推送消息") public String pushNotification(@RequestParam String registrationId, @RequestParam String message) { - jPushService.sendPushNotification(registrationId, message); + //jPushService.sendPushNotification(registrationId, message); return "Push request sent!"; } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java index 7800c230..d851c843 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallOrderMapper.java @@ -32,4 +32,6 @@ public interface MallOrderMapper extends BaseMapper { Integer selectPendingBadReviewCount(@Param("shopId") String shopId); Integer selectRefundCount(@Param("shopId") String shopId); + + List getWeChatId(); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java new file mode 100644 index 00000000..b89de096 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java @@ -0,0 +1,157 @@ +package cc.hiver.mall.mq; + +import cc.hiver.core.entity.User; +import cc.hiver.core.entity.Worker; +import cc.hiver.core.service.UserService; +import cc.hiver.core.service.WorkerService; +import cc.hiver.mall.dao.mapper.MallDeliveryOrderMapper; +import cc.hiver.mall.dao.mapper.MallOrderMapper; +import cc.hiver.mall.entity.MallDeliveryOrder; +import cc.hiver.mall.pojo.dto.CreateOrderDTO; +import cc.hiver.mall.serviceimpl.mybatis.MallOrderServiceImpl; +import cc.hiver.mall.utils.WechatSendMessageUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.mp.bean.template.WxMpTemplateData; +import org.apache.commons.lang3.StringUtils; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * 处理非核心、响应时间不敏感的常规异步队列。 + */ +@Slf4j +@Component +public class OrderAsyncConsumer { + + @Autowired + private MallOrderServiceImpl mallOrderService; + + @Autowired + private UserService userService; + + @Autowired + private MallDeliveryOrderMapper mallDeliveryOrderMapper; + + @Autowired + private MallOrderMapper mallOrderMapper; + + @Autowired + private WorkerService workerService; + + @Autowired + private WechatSendMessageUtil wechatSendMessageUtil; + + // 微信模板配置 + private static final String TEMPLATE_ID = "K15zpZSHBNivouTfpW5FK1XFz8GbYAK8b9dXXOP_Ka0"; + + @RabbitListener(queues = OrderQueueConfig.ASYNC_WORKER_QUEUE) + public void handleWechatNotify(String message) { + log.info("【异步队列处理】微信模版推送: {}", message); + try { + JSONObject msg = JSON.parseObject(message); + String orderId = msg.getString("orderId"); + + // 查询配送单 + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(MallDeliveryOrder::getOrderId, orderId); + MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(qw); + if (delivery == null) { + return; + } + + List targetOpenIds = new ArrayList<>(); + String orderTypeStr = "抢单大厅单"; + + if (StringUtils.isNotBlank(delivery.getWorkerId())) { + orderTypeStr = "指定接单"; + // 获取指定骑手的关联 userId 和 openid + String openId = getOfficialAccountOpenidByWorkerId(delivery.getWorkerId()); + if (StringUtils.isNotBlank(openId)) { + targetOpenIds.add(openId); + } + } else { + // 如果没有指定人,发给所有人?业务需求:给所有配送员发微信公众号消息 + // 这可能需要查出所有 Worker 关联的 user.officialAccountOpenid (实际应用中可能需要加状态过滤) + // 为简化并实现逻辑,这里只查有效的骑手 user + targetOpenIds = getAllWorkerOpenIds(); + } + + if (targetOpenIds.isEmpty()) { + log.info("【异步队列处理】没有找到可推送微信模板的openid,目标跳过推送。orderId={}", orderId); + return; + } + + // 组装模板参数 (参照要求配置) + List data = new ArrayList<>(); + data.add(new WxMpTemplateData("thing3", orderTypeStr)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String timeFormat = delivery.getCreateTime() != null ? sdf.format(delivery.getCreateTime()) : sdf.format(new java.util.Date()); + data.add(new WxMpTemplateData("time4", timeFormat)); + + String address = StringUtils.defaultString(delivery.getShopName()) + " - " + StringUtils.defaultString(delivery.getReceiverAddress()); + // 模板参数有长度限制(通常20字),做简略截断以防发送失败 + if (address.length() > 20) { + address = address.substring(0, 17) + "..."; + } + data.add(new WxMpTemplateData("thing10", address)); + + // 查询或者组装此单据的物品数量,delivery可能没有存数量,这里可简化显示。 + // 由于MallDeliveryOrder中可能没存总量,可通过orderId反查Goods(篇幅及耗时原因,也可以在发送消息时传进来) + // 查出 MallDeliveryOrder 自带或者去 MallOrderGood + data.add(new WxMpTemplateData("character_string15", "1")); // 也可以去 mall_order_goods 查数量 + + data.add(new WxMpTemplateData("phrase8", "待接单")); + + wechatSendMessageUtil.sendWechatTempMessage(targetOpenIds, TEMPLATE_ID, data, "pages/index/index"); + log.info("【异步队列处理】微信模版推送成功,推送总人数:{}", targetOpenIds.size()); + } catch (Exception e) { + log.error("【异步队列处理】微信推送异常(不影响主链路): {}", e.getMessage(), e); + } + } + + + private String getOfficialAccountOpenidByWorkerId(String workerId) { + Worker worker = workerService.findByWorkerId(workerId); + if (worker != null && StringUtils.isNotBlank(worker.getUserId())) { + User user = userService.findById(worker.getUserId()); + if (user != null) { + return user.getOfficialAccountOpenid(); + } + } + return null; + } + + private List getAllWorkerOpenIds() { + return mallOrderMapper.getWeChatId(); + } + + @RabbitListener(queues = OrderQueueConfig.ASYNC_CACHE_QUEUE) + public void handleCacheUpdate(String message) { + log.info("【异步队列处理】刷新订单附属缓存: {}", message); + try { + JSONObject body = JSON.parseObject(message); + String shopId = body.getString("shopId"); + String regionId = body.getString("regionId"); + Integer shopSaleCount = body.getInteger("shopSaleCount"); + JSONArray itemsArray = body.getJSONArray("items"); + // 将JSON结构重建后,委托回原有 Service 逻辑执行 + List items = new ArrayList<>(); + for (int i = 0; i < itemsArray.size(); i++) { + items.add(itemsArray.getJSONObject(i).toJavaObject(CreateOrderDTO.OrderItemDTO.class)); + } + // productMap 重建,为避免过度依赖对象序列化,可以直接在 redis 增量方法里面反查 + mallOrderService.updateSaleCacheIncrementalViaMq(shopId, regionId, items, shopSaleCount); + } catch (Exception e) { + log.error("【异步队列处理】缓存更新失败: {}", e.getMessage(), e); + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java new file mode 100644 index 00000000..1f738d2e --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncProducer.java @@ -0,0 +1,66 @@ +package cc.hiver.mall.mq; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 订单相关的MQ异步消息发送者 + */ +@Slf4j +@Component +public class OrderAsyncProducer { + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * 发送微信模板推送请求 (包含参数,接收人标识等) + */ + public void sendWechatNotify(String orderId) { + Map msg = new HashMap<>(); + msg.put("orderId", orderId); + rabbitTemplate.convertAndSend(OrderQueueConfig.ORDER_DIRECT_EXCHANGE, OrderQueueConfig.ASYNC_WORKER_ROUTING, JSON.toJSONString(msg)); + log.info("【订单MQ】已发出微信通知异步处理委托, orderId={}", orderId); + } + + /** + * 发送异步缓存更新和非核心数据统计任务 + */ + public void sendCacheUpdate(Map params) { + rabbitTemplate.convertAndSend(OrderQueueConfig.ORDER_DIRECT_EXCHANGE, OrderQueueConfig.ASYNC_CACHE_ROUTING, JSON.toJSONString(params)); + } + + /** + * 发送延迟(死信)超时消息 + * @param orderId 订单ID + * @param delayType 延迟监听类型(如:Worker_Timeout_5m, Worker_Timeout_10m, Shop_Cook_Timeout) + * @param delayMillis 需要延迟的毫秒数 + */ + public void sendDelayMessage(String orderId, String delayType, long delayMillis) { + Map msgBody = new HashMap<>(); + msgBody.put("orderId", orderId); + msgBody.put("delayType", delayType); + msgBody.put("timestamp", System.currentTimeMillis()); + + rabbitTemplate.convertAndSend(OrderQueueConfig.DELAY_EXCHANGE, OrderQueueConfig.DELAY_ROUTING, JSON.toJSONString(msgBody), + new MessagePostProcessor() { + @Override + public Message postProcessMessage(Message message) throws AmqpException { + // 设置每条消息的 TTL (存活时间) + message.getMessageProperties().setExpiration(String.valueOf(delayMillis)); + return message; + } + }); + + log.info("【订单MQ】已发送一条{}延时校验消息,设定在{}ms后触发,orderId={}", delayType, delayMillis, orderId); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java new file mode 100644 index 00000000..25a91c0f --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java @@ -0,0 +1,135 @@ +package cc.hiver.mall.mq; + +import cc.hiver.core.common.constant.SettingConstant; +import cc.hiver.core.common.sms.SmsUtil; +import cc.hiver.core.serviceimpl.JPushServiceImpl; +import cc.hiver.mall.dao.mapper.MallDeliveryOrderMapper; +import cc.hiver.mall.dao.mapper.MallOrderMapper; +import cc.hiver.mall.dao.mapper.ShopMapper; +import cc.hiver.mall.entity.MallDeliveryOrder; +import cc.hiver.mall.entity.MallOrder; +import cc.hiver.mall.entity.Shop; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 订单超时控制死信队列消费者 + */ +@Slf4j +@Component +public class OrderDelayConsumer { + + @Autowired + private MallDeliveryOrderMapper mallDeliveryOrderMapper; + + @Autowired + private MallOrderMapper mallOrderMapper; + + @Autowired + private ShopMapper shopMapper; + + @Autowired + private SmsUtil smsUtil; + + @Autowired + private JPushServiceImpl jPushService; + + @RabbitListener(queues = OrderQueueConfig.DEAD_QUEUE) + public void processDelayMessage(String messageBody) { + log.info("【订单延时处理】接收到死信超时消息: {}", messageBody); + try { + JSONObject msg = JSON.parseObject(messageBody); + String orderId = msg.getString("orderId"); + String delayType = msg.getString("delayType"); + + switch (delayType) { + case "Worker_Timeout_5m": + handleWorkerTimeout5m(orderId); + break; + case "Worker_Timeout_10m": + handleWorkerTimeout10m(orderId); + break; + case "NoWorker_Timeout_10m": + handleNoWorkerTimeout10m(orderId); + break; + case "Shop_Cook_Timeout": + handleShopCookTimeout(orderId); + break; + default: + log.warn("【订单延时处理】未知的延迟类型: {}", delayType); + } + } catch (Exception e) { + log.error("【订单延时处理】处理超时消息异常: {}", e.getMessage(), e); + } + } + + private void handleWorkerTimeout5m(String orderId) { + MallDeliveryOrder delivery = getDeliveryOrder(orderId); + if (delivery == null || delivery.getStatus() != 0 || StringUtils.isBlank(delivery.getWorkerId())) { + // 已被处理、状态不再是0待接单,或者已经不是指定单,安全撤销(忽略) + return; + } + log.info("【订单延时处理】指定配送单 5分钟未接单,向配送员发送短信通知 orderId={}", orderId); + // 发送 SMS_TIMEOUT + if (StringUtils.isNotBlank(delivery.getWorkerPhone())) { + smsUtil.sendCode(delivery.getWorkerPhone(), null,SettingConstant.SMS_TYPE.SMS_TIMEOUT.name()); + } + } + + private void handleWorkerTimeout10m(String orderId) { + MallDeliveryOrder delivery = getDeliveryOrder(orderId); + if (delivery == null || delivery.getStatus() != 0 || StringUtils.isBlank(delivery.getWorkerId())) { + return; + } + log.info("【订单延时处理】指定配送单 10分钟未接单,移除配送员并通知用户 orderId={}", orderId); + // 更新配送单(去掉workerId,退回大厅) + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(MallDeliveryOrder::getId, delivery.getId()) + .set(MallDeliveryOrder::getWorkerId, (String)null).set(MallDeliveryOrder::getWorkerName, (String)null) + .set(MallDeliveryOrder::getWorkerPhone, (String)null); + mallDeliveryOrderMapper.update(null, uw); + + // 给用户通知 + if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { + smsUtil.sendCode(delivery.getReceiverPhone(), null,SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); + } + } + + private void handleNoWorkerTimeout10m(String orderId) { + MallDeliveryOrder delivery = getDeliveryOrder(orderId); + if (delivery == null || delivery.getStatus() != 0 || StringUtils.isNotBlank(delivery.getWorkerId())) { + return; + } + log.info("【订单延时处理】大厅抢单 10分钟无人接单,通知用户 orderId={}", orderId); + if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { + smsUtil.sendCode(delivery.getReceiverPhone(),null, SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); + } + } + + private void handleShopCookTimeout(String orderId) { + MallOrder order = mallOrderMapper.selectById(orderId); + if (order == null || order.getShopMakeTime() != null) { + // 商家已经点击了出餐 (shopMakeTime != null) + return; + } + log.info("【订单延时处理】商家超时未点击出餐,向商家发送极光推送 orderId={}", orderId); + Shop shop = shopMapper.selectById(order.getShopId()); + if (shop != null && StringUtils.isNotBlank(shop.getClientId())) { + // 发送给商家端的 APP Push + jPushService.sendPushNotification(shop.getClientId(), "您的订单出餐已超时,请尽快处理!", orderId); + } + } + + private MallDeliveryOrder getDeliveryOrder(String orderId) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(MallDeliveryOrder::getOrderId, orderId); + return mallDeliveryOrderMapper.selectOne(qw); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderQueueConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderQueueConfig.java new file mode 100644 index 00000000..add55809 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderQueueConfig.java @@ -0,0 +1,104 @@ +package cc.hiver.mall.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +/** + * 订单流程 MQ 配置,包括普通的异步任务和死信超时延迟任务。 + */ +@Configuration +public class OrderQueueConfig { + + public static final String ORDER_DIRECT_EXCHANGE = "order.direct.exchange"; + + // 异步业务(如微信通知,缓存更新等) + public static final String ASYNC_WORKER_QUEUE = "order.async.worker.queue"; + public static final String ASYNC_WORKER_ROUTING = "order.async.worker.routing.key"; + + public static final String ASYNC_CACHE_QUEUE = "order.async.cache.queue"; + public static final String ASYNC_CACHE_ROUTING = "order.async.cache.routing.key"; + + // 延时消息 DLX 配置 + // 原理:将消息发至 delay 队列(没有消费者),等过期后,由 rabbitmq 自动抛入 dead 交换机,最终进入 dead 队列供消费。 + public static final String DELAY_EXCHANGE = "order.delay.exchange"; + public static final String DELAY_QUEUE = "order.delay.queue"; + public static final String DELAY_ROUTING = "order.delay.routing.key"; + + public static final String DEAD_EXCHANGE = "order.dead.exchange"; + public static final String DEAD_QUEUE = "order.dead.queue"; + public static final String DEAD_ROUTING = "order.dead.routing.key"; + + // ----------------- 常规业务配置 ----------------- + + @Bean + public DirectExchange orderDirectExchange() { + return new DirectExchange(ORDER_DIRECT_EXCHANGE, true, false); + } + + @Bean + public Queue asyncWorkerQueue() { + return new Queue(ASYNC_WORKER_QUEUE, true); + } + + @Bean + public Binding bindingAsyncWorkerQueue() { + return BindingBuilder.bind(asyncWorkerQueue()).to(orderDirectExchange()).with(ASYNC_WORKER_ROUTING); + } + + @Bean + public Queue asyncCacheQueue() { + return new Queue(ASYNC_CACHE_QUEUE, true); + } + + @Bean + public Binding bindingAsyncCacheQueue() { + return BindingBuilder.bind(asyncCacheQueue()).to(orderDirectExchange()).with(ASYNC_CACHE_ROUTING); + } + + // ----------------- 死信(延时)架构配置 ----------------- + + @Bean + public DirectExchange delayExchange() { + return new DirectExchange(DELAY_EXCHANGE, true, false); + } + + @Bean + public DirectExchange deadExchange() { + return new DirectExchange(DEAD_EXCHANGE, true, false); + } + + @Bean + public Queue delayQueue() { + // 设置该队列的死信去向 + Map args = new HashMap<>(2); + // x-dead-letter-exchange: 出现 dead letter 的时候将 letter 发送到这个 exchange + args.put("x-dead-letter-exchange", DEAD_EXCHANGE); + // x-dead-letter-routing-key: 出现 dead letter 的时候将 letter 路由给该 routing-key + args.put("x-dead-letter-routing-key", DEAD_ROUTING); + // 这是一个没有消费者的等待队列,消息在这里过期后会进入死信交换机 + return new Queue(DELAY_QUEUE, true, false, false, args); + } + + @Bean + public Binding bindingDelayQueue() { + return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_ROUTING); + } + + @Bean + public Queue deadQueue() { + // 这是真正消费超时的死信队列 + return new Queue(DEAD_QUEUE, true); + } + + @Bean + public Binding bindingDeadQueue() { + return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(DEAD_ROUTING); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementConfirmConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementConfirmConsumer.java new file mode 100644 index 00000000..4a1dba57 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementConfirmConsumer.java @@ -0,0 +1,63 @@ +package cc.hiver.mall.mq; + +import cc.hiver.mall.controller.AdminSettlementController.ConfirmShopReq; +import cc.hiver.mall.entity.MallSettlementRecord; +import cc.hiver.mall.service.mybatis.MallSettlementRecordService; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 商家结算MQ消费者 + */ +@Slf4j +@Component +public class SettlementConfirmConsumer { + + @Autowired + private MallSettlementRecordService mallSettlementRecordService; + + @RabbitListener(queues = SettlementMqConfig.SETTLEMENT_CONFIRM_QUEUE) + public void handleSettlementConfirm(String message) { + log.info("【MQ结算消费者】接收到按商家确认结算消息: {}", message); + try { + ConfirmShopReq req = JSON.parseObject(message, ConfirmShopReq.class); + if (req == null || req.getShopIds() == null || req.getShopIds().isEmpty()) { + log.warn("【MQ结算消费者】消息参数异常(商家ID列表为空), message={}", message); + return; + } + + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(req.getRegionId())) { + qw.eq(MallSettlementRecord::getRegionId, req.getRegionId()); + } + if (StringUtils.isNotBlank(req.getSettlementDate())) { + qw.apply("DATE(create_time) = {0}", req.getSettlementDate()); + } + qw.in(MallSettlementRecord::getShopId, req.getShopIds()); + qw.eq(MallSettlementRecord::getStatus, 0); + + List list = mallSettlementRecordService.list(qw); + if (list == null || list.isEmpty()) { + log.info("【MQ结算消费者】没有需要确认的结算记录,终止处理"); + return; + } + + List recordIds = list.stream().map(MallSettlementRecord::getId).collect(Collectors.toList()); + mallSettlementRecordService.confirmSettlements(recordIds); + log.info("【MQ结算消费者】结算确认处理成功, 共处理了 {} 条记录", recordIds.size()); + + } catch (Exception e) { + log.error("【MQ结算消费者】处理结算消息异常: {}", e.getMessage(), e); + // 此处由于是简单的配置,如果出现异常会根据 application.yml 中的重试机制进行重试(max-attempts: 3) + throw new RuntimeException(e); // 抛出异常以触发重试或放入死信队列 + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementMqConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementMqConfig.java new file mode 100644 index 00000000..71b89ddd --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/SettlementMqConfig.java @@ -0,0 +1,35 @@ +package cc.hiver.mall.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 商家结算MQ生产环境配置 + */ +@Configuration +public class SettlementMqConfig { + + public static final String SETTLEMENT_EXCHANGE = "mall.settlement.exchange"; + public static final String SETTLEMENT_CONFIRM_QUEUE = "mall.settlement.confirm.queue"; + public static final String SETTLEMENT_CONFIRM_ROUTING_KEY = "mall.settlement.confirm.routing.key"; + + @Bean + public DirectExchange settlementExchange() { + return new DirectExchange(SETTLEMENT_EXCHANGE, true, false); + } + + @Bean + public Queue settlementConfirmQueue() { + // durable=true 持久化队列 + return new Queue(SETTLEMENT_CONFIRM_QUEUE, true); + } + + @Bean + public Binding bindingSettlementConfirmQueue() { + return BindingBuilder.bind(settlementConfirmQueue()).to(settlementExchange()).with(SETTLEMENT_CONFIRM_ROUTING_KEY); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java index 3bdc3237..e0cb333d 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java @@ -99,6 +99,8 @@ public class CreateOrderDTO { public static class WorkerParam { @ApiModelProperty(value = "配送员ID", required = true) private String workerId; + @ApiModelProperty(value = "配送员手机号", required = true) + private String workerPhone; @ApiModelProperty(value = "配送员名称", required = true) private String workerName; @ApiModelProperty(value = "配送员规则佣金 orderBkge", required = true) diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallOrderService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallOrderService.java index 78dd91fc..39abf30f 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallOrderService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallOrderService.java @@ -70,6 +70,8 @@ public interface MallOrderService extends IService { */ void userRequireMake(String orderId); + void updateSaleCacheIncrementalViaMq(String shopId, String regionId, java.util.List items, Integer shopSaleCount); + /** * 商家拒绝退款 */ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/SendMessageServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/SendMessageServiceImpl.java index 76f1997b..122deff4 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/SendMessageServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/SendMessageServiceImpl.java @@ -21,7 +21,7 @@ public class SendMessageServiceImpl implements SendMessageService { Worker worker = workerService.findById(workerId); String registrationId = worker.getClientId(); String message = "您有一笔新的订单,请注意查看"; - jPushService.sendPushNotification(registrationId, message); + //jPushService.sendPushNotification(registrationId, message); } @Override @@ -29,7 +29,7 @@ public class SendMessageServiceImpl implements SendMessageService { Worker worker = workerService.findById(workerId); String registrationId = worker.getClientId(); String message = "您有一笔订单被取消!"; - jPushService.sendPushNotification(registrationId, message); + //jPushService.sendPushNotification(registrationId, message); } @Override diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java index 2621fed3..9c7f7a92 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java @@ -1,16 +1,23 @@ package cc.hiver.mall.serviceimpl.mybatis; +import cc.hiver.core.common.constant.SettingConstant; +import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.entity.Worker; +import cc.hiver.core.serviceimpl.JPushServiceImpl; import cc.hiver.core.serviceimpl.WorkerServiceImpl; import cc.hiver.mall.dao.mapper.MallDeliveryOrderMapper; import cc.hiver.mall.dao.mapper.MallOrderGoodsMapper; import cc.hiver.mall.dao.mapper.MallOrderGroupMapper; +import cc.hiver.mall.dao.mapper.ShopTakeawayMapper; import cc.hiver.mall.entity.*; +import cc.hiver.mall.mq.OrderAsyncProducer; import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; import cc.hiver.mall.pojo.query.MallRefundRecordPageQuery; +import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.mybatis.MallDeliveryOrderService; import cc.hiver.mall.service.mybatis.MallOrderService; import cc.hiver.mall.service.mybatis.MallRefundRecordService; +import cc.hiver.mall.service.mybatis.MallUserCouponService; import cc.hiver.mall.utils.MerchantOrderSeqUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; @@ -56,6 +63,21 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl pageDelivery(MallDeliveryOrderPageQuery q) { @@ -119,7 +141,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl orderIdsSend = new ArrayList<>(); // 更新配送单 Date createTime = new Timestamp(System.currentTimeMillis()); // 1. 获取当前时间 (LocalDateTime) @@ -136,6 +158,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl orderIds = Arrays.asList(group.getGroupOrderIds().split(",")); + orderIdsSend = orderIds; for (String orderId : orderIds) { MallOrder orderInner = mallOrderService.getById(orderId); if (orderInner != null) { @@ -152,6 +176,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl 配送中(或待取货) if (StringUtils.isNotBlank(delivery.getOrderId())) { if (order != null && order.getStatus() == ORDER_STATUS_WAIT_DELIVERY) { + orderIdsSend.add(delivery.getOrderId()); String latestSeq = merchantOrderSeqUtil.generateOrderSequence(order.getShopId(), 1); LambdaUpdateWrapper oUw = new LambdaUpdateWrapper<>(); oUw.eq(MallOrder::getId, delivery.getOrderId()) .set(MallOrder::getNumberCode, latestSeq).set(MallOrder::getStatus, 3); // 待取货 //缺少根据groupId更新面对面团所有订单状态 mallOrderService.update(oUw); + jPushService.sendPushNotification(shop.getClientId(), "您有一笔新的订单",delivery.getOrderId()); numberCode = latestSeq; } } @@ -173,12 +200,14 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl 配送中(或待取货) if (StringUtils.isNotBlank(delivery.getOrderId())) { if (order != null && order.getStatus() == ORDER_STATUS_WAIT_DELIVERY) { + orderIdsSend.add(delivery.getOrderId()); String latestSeq = merchantOrderSeqUtil.generateOrderSequence(order.getShopId(), 1); LambdaUpdateWrapper oUw = new LambdaUpdateWrapper<>(); oUw.eq(MallOrder::getId, delivery.getOrderId()) .set(MallOrder::getNumberCode, latestSeq).set(MallOrder::getStatus, 3); // 待取货 //缺少根据groupId更新面对面团所有订单状态 mallOrderService.update(oUw); + jPushService.sendPushNotification(shop.getClientId(), "您有一笔新的订单",delivery.getOrderId()); numberCode = latestSeq; } } @@ -186,10 +215,32 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl orderId) { + orderId.forEach(id -> { + MallOrder order = mallOrderService.getById(id); + if (order != null) { + long cookTimeMins = 10; // default + try { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId()); + ShopTakeaway shopTakeaway = shopTakeawayMapper.selectOne(qw); + if (shopTakeaway != null) { + cookTimeMins = shopTakeaway.getCookingTime(); + } + } catch (Exception e) { + log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + } + orderAsyncProducer.sendDelayMessage(id, "Shop_Cook_Timeout", cookTimeMins * 60 * 1000L); + } + }); + } + /** * 配送员取货(状态:待取货 -> 配送中) */ @@ -295,10 +346,15 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); uw.eq(MallDeliveryOrder::getId, deliveryId) - .set(MallDeliveryOrder::getWorkerId, null); + .set(MallDeliveryOrder::getWorkerId, null).set(MallDeliveryOrder::getWorkerName, (String)null) + .set(MallDeliveryOrder::getWorkerPhone, (String)null); this.update(uw); + //给用户发送短信通知被拒绝 + smsUtil.sendCode(delivery.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_REJECT_ORDER.name()); } // ================================================================ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java index d2cabeda..c5ef6eae 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java @@ -51,6 +51,9 @@ public class MallOrderGroupServiceImpl extends ServiceImpl dqw = new LambdaQueryWrapper<>(); dqw.eq(MallDeliveryOrder::getOrderId, order.getId()).last("LIMIT 1"); MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dqw); - + //mq处理 + triggerDeliveryAsyncEvents(order.getId(),delivery); if (delivery != null) { if (StringUtils.isNotBlank(delivery.getWorkerId())) { boolean isValid = false; @@ -189,6 +192,27 @@ public class MallOrderGroupServiceImpl extends ServiceImpl().eq("id",dto.getShopId()).set("sale_count",shopSaleCount)); } - // 增量更新Redis缓存(店铺销量 + 商品销量),避免走refreshShopCache全量查询 + // 增量更新Redis缓存(店铺销量 + 商品销量),改为交给MQ异步处理 try { - updateSaleCacheIncremental(dto.getShopId(), dto.getRegionId(), dto.getItems(), productMap, shopSaleCount); + java.util.Map asyncParams = new java.util.HashMap<>(); + asyncParams.put("shopId", dto.getShopId()); + asyncParams.put("regionId", dto.getRegionId()); + asyncParams.put("items", dto.getItems()); + asyncParams.put("shopSaleCount", shopSaleCount); + orderAsyncProducer.sendCacheUpdate(asyncParams); } catch (Exception e) { - log.warn("增量更新Redis销量缓存失败,不影响下单: {}", e.getMessage()); + log.warn("推送Redis销量缓存异步任务失败,不影响下单: {}", e.getMessage()); } BigDecimal packageFee = dto.getPackageFee() != null ? dto.getPackageFee() : BigDecimal.ZERO; @@ -475,6 +492,9 @@ public class MallOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); @@ -523,6 +543,54 @@ public class MallOrderServiceImpl extends ServiceImpl dw = new LambdaQueryWrapper<>(); + dw.eq(MallDeliveryOrder::getOrderId, orderId); + MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(dw); + if (delivery != null) { + boolean isSpecified = StringUtils.isNotBlank(delivery.getWorkerId()); + if (isSpecified) { + // 指定单:5分钟校验,10分钟校验 + orderAsyncProducer.sendDelayMessage(orderId, "Worker_Timeout_5m", 5 * 60 * 1000L); + orderAsyncProducer.sendDelayMessage(orderId, "Worker_Timeout_10m", 10 * 60 * 1000L); + } else { + // 抢单大厅:10分钟校验 + orderAsyncProducer.sendDelayMessage(orderId, "NoWorker_Timeout_10m", 10 * 60 * 1000L); + } + } + + // 如果需要检测出餐超时,发一条出餐超时延时消息 + //triggerShopCookTimeoutEvent(orderId); + } + + @Autowired + private cc.hiver.mall.dao.mapper.ShopTakeawayMapper shopTakeawayMapper; + + private void triggerShopCookTimeoutEvent(String orderId) { + MallOrder order = this.getById(orderId); + if (order != null) { + long cookTimeMins = 15; // default + try { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(cc.hiver.mall.entity.ShopTakeaway::getShopId, order.getShopId()); + List list = shopTakeawayMapper.selectList(qw); + if (list != null && !list.isEmpty() && list.get(0).getCookingTime() != null) { + cookTimeMins = list.get(0).getCookingTime(); + } + } catch (Exception e) { + log.error("获取 ShopTakeaway cookingTime 异常: {}", e.getMessage()); + } + orderAsyncProducer.sendDelayMessage(orderId, "Shop_Cook_Timeout", cookTimeMins * 60 * 1000L); + } + } + @Override @Transactional(rollbackFor = Exception.class) public void shopReject(String orderId, String reason) { @@ -870,11 +938,17 @@ public class MallOrderServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); uw.eq(MallOrder::getId, orderId).set(MallOrder::getUserRequireMake,1); this.update(uw); + jPushService.sendPushNotification(shopService.findById(order.getShopId()).getClientId(), "您有一笔新的到店订单",orderId); + + // 触发商家出餐超时监听 + triggerShopCookTimeoutEvent(orderId); } @Override @@ -1234,6 +1308,7 @@ public class MallOrderServiceImpl extends ServiceImpl goodsList = mallOrderGoodsMapper.selectByOrderId(order.getId()); if (goodsList == null || goodsList.isEmpty()) return; List returnList = goodsList.stream() @@ -1585,4 +1667,14 @@ public class MallOrderServiceImpl extends ServiceImpl items, Integer shopSaleCount) { + // 反查 productMap + List productIds = items.stream().map(CreateOrderDTO.OrderItemDTO::getProductId).collect(Collectors.toList()); + List productList = productMapper.selectBatchIds(productIds); + Map productMap = productList.stream().collect(Collectors.toMap(Product::getId, p -> p)); + + updateSaleCacheIncremental(shopId, regionId, items, productMap, shopSaleCount); + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatSendMessageUtil.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatSendMessageUtil.java new file mode 100644 index 00000000..2fa2c63a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatSendMessageUtil.java @@ -0,0 +1,73 @@ +package cc.hiver.mall.utils; + +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.template.WxMpTemplateData; +import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 微信推送模板消息工具类 + * + * @author Yazhi Li + */ +@Component +public class WechatSendMessageUtil { + @Autowired + private WxMpService wxMpService; + + @Value("${hiver.social.wechat.appId}") + private String appId; + + private static final Logger log = LoggerFactory.getLogger(WechatSendMessageUtil.class); + public void sendWechatTempMessage(List officialAccountOpenids,String templateId, List data,String path) throws Exception { + for (String openId : officialAccountOpenids) { + + final WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() + // 要推送的用户openid + .toUser(openId) + // 消息详情跳转地址:https://blog.csdn.net/qq_34272760/article/details/120152903 + // 若需要跳转小程序,url则设置为:http://mp.weixin.qq.com,然后设置相关的MiniProgram参数【跳转的小程序必须是公众号关联的小程序!!!】 + .url("http://mp.weixin.qq.com") + // 微信模板ID + .templateId(templateId) + .build(); + // data的字段及内容是自定义的,不必按我这儿的来,具体怎么和已有的系统消息结合,实现key和color可配置化需自行考虑 + //final JsonObject jsonObject = JsonParser.parseString(weChatServerMsgVo.getContent()).getAsJsonObject(); + /*final List data = new ArrayList<>(); + for (String key : jsonObject.keySet()) { + final Object value = jsonObject.get(key); + String afterValue = String.valueOf(value).replace("\"", ""); + log.info("Key: " + key + ", Value: " + afterValue); + data.add(new WxMpTemplateData(key, afterValue)); + + }*/ + templateMessage.setData(data); + + // 跳转小程序相关配置【跳转的小程序必须是公众号关联的小程序!!!】 + final WxMpTemplateMessage.MiniProgram miniProgram = new WxMpTemplateMessage.MiniProgram(); + log.info("appId============" + appId); + // 小程序的appId + miniProgram.setAppid(appId); + // 小程序的pagePath 注意,这里是支持传参的!!! + miniProgram.setPagePath(path); + // 需要跳转首页时,需要设置 usePath = true (默认是 false,只跳转非首页) + miniProgram.setUsePath(true); + templateMessage.setMiniProgram(miniProgram); + + try { + // 发送模板消息 + wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); + } catch (Exception e) { + log.error("微信公众号模板消息推送失败,接收userId: " + openId, e); + } + + } + } + +} diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml index 60d16ed8..cb116a26 100644 --- a/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml @@ -195,4 +195,8 @@ + +