Commit c90ac42ac06be2726cee3a26b789cd8c09b7a69f

Authored by shaofan
1 parent 0fc95c77

新增消息中心

src/main/java/com/diligrp/rider/controller/AdminMessageController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.entity.RiderMessageTemplate;
  5 +import com.diligrp.rider.service.AdminMessageService;
  6 +import jakarta.servlet.http.HttpServletRequest;
  7 +import lombok.Data;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.util.List;
  12 +import java.util.Map;
  13 +
  14 +/**
  15 + * 管理端消息接口
  16 + */
  17 +@RestController
  18 +@RequestMapping("/api/admin/message")
  19 +@RequiredArgsConstructor
  20 +public class AdminMessageController {
  21 +
  22 + private final AdminMessageService messageService;
  23 +
  24 + @Data
  25 + static class MessageSendDTO {
  26 + private Long riderId;
  27 + private Integer type;
  28 + private String title;
  29 + private String content;
  30 + private String bizType;
  31 + private Long bizId;
  32 + }
  33 +
  34 + @Data
  35 + static class MessageBroadcastDTO {
  36 + private Integer type;
  37 + private String title;
  38 + private String content;
  39 + }
  40 +
  41 + /**
  42 + * 发送消息给指定骑手
  43 + */
  44 + @PostMapping("/send")
  45 + public Result<Void> send(@RequestBody MessageSendDTO dto, HttpServletRequest request) {
  46 + Long cityId = resolveCityId(request);
  47 + messageService.sendToRider(cityId, dto.getRiderId(), dto.getType(), dto.getTitle(), dto.getContent(), dto.getBizType(), dto.getBizId());
  48 + return Result.success();
  49 + }
  50 +
  51 + /**
  52 + * 群发消息(城市内所有骑手)
  53 + */
  54 + @PostMapping("/broadcast")
  55 + public Result<Void> broadcast(@RequestBody MessageBroadcastDTO dto, HttpServletRequest request) {
  56 + Long cityId = resolveCityId(request);
  57 + messageService.broadcastToCity(cityId, dto.getType(), dto.getTitle(), dto.getContent());
  58 + return Result.success();
  59 + }
  60 +
  61 + /**
  62 + * 查询消息列表(管理端)
  63 + */
  64 + @GetMapping("/list")
  65 + public Result<Map<String, Object>> list(
  66 + @RequestParam(required = false) Long riderId,
  67 + @RequestParam(required = false) Integer type,
  68 + @RequestParam(defaultValue = "1") Integer page,
  69 + HttpServletRequest request) {
  70 + Long cityId = resolveCityId(request);
  71 + return Result.success(messageService.listMessages(cityId, riderId, type, page));
  72 + }
  73 +
  74 + /**
  75 + * 获取模板列表
  76 + */
  77 + @GetMapping("/templates")
  78 + public Result<List<RiderMessageTemplate>> templates() {
  79 + return Result.success(messageService.listTemplates());
  80 + }
  81 +
  82 + /**
  83 + * 解析城市ID(分站管理员自动绑定本城市,超管需要传递)
  84 + */
  85 + private Long resolveCityId(HttpServletRequest request) {
  86 + if ("substation".equals(request.getAttribute("role"))) {
  87 + return (Long) request.getAttribute("cityId");
  88 + }
  89 + // 超管需要从请求参数获取 cityId(这里简化处理,实际应该从请求参数获取)
  90 + return (Long) request.getAttribute("cityId");
  91 + }
  92 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderMessageController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.service.RiderMessageService;
  5 +import jakarta.servlet.http.HttpServletRequest;
  6 +import lombok.RequiredArgsConstructor;
  7 +import org.springframework.web.bind.annotation.*;
  8 +
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +
  12 +/**
  13 + * 骑手端消息接口
  14 + */
  15 +@RestController
  16 +@RequestMapping("/api/rider/message")
  17 +@RequiredArgsConstructor
  18 +public class RiderMessageController {
  19 +
  20 + private final RiderMessageService messageService;
  21 +
  22 + /**
  23 + * 获取消息列表(分页)
  24 + *
  25 + * @param type 消息类型:1=订单 2=系统 null=全部
  26 + * @param page 页码
  27 + * @param pageSize 每页数量
  28 + */
  29 + @GetMapping("/list")
  30 + public Result<Map<String, Object>> list(
  31 + @RequestParam(required = false) Integer type,
  32 + @RequestParam(defaultValue = "1") Integer page,
  33 + @RequestParam(defaultValue = "20") Integer pageSize,
  34 + HttpServletRequest request) {
  35 + Long riderId = (Long) request.getAttribute("riderId");
  36 + return Result.success(messageService.listByRider(riderId, type, page, pageSize));
  37 + }
  38 +
  39 + /**
  40 + * 获取未读消息数
  41 + */
  42 + @GetMapping("/unread-count")
  43 + public Result<Map<String, Integer>> unreadCount(HttpServletRequest request) {
  44 + Long riderId = (Long) request.getAttribute("riderId");
  45 + return Result.success(messageService.getUnreadCount(riderId));
  46 + }
  47 +
  48 + /**
  49 + * 标记消息为已读
  50 + *
  51 + * @param messageId 消息ID
  52 + */
  53 + @PostMapping("/read")
  54 + public Result<Void> markRead(@RequestParam Long messageId, HttpServletRequest request) {
  55 + Long riderId = (Long) request.getAttribute("riderId");
  56 + messageService.markRead(riderId, messageId);
  57 + return Result.success();
  58 + }
  59 +
  60 + /**
  61 + * 批量标记已读
  62 + *
  63 + * @param messageIds 消息ID列表
  64 + */
  65 + @PostMapping("/read-batch")
  66 + public Result<Void> markReadBatch(@RequestBody List<Long> messageIds, HttpServletRequest request) {
  67 + Long riderId = (Long) request.getAttribute("riderId");
  68 + messageService.markReadBatch(riderId, messageIds);
  69 + return Result.success();
  70 + }
  71 +
  72 + /**
  73 + * 全部标记已读
  74 + *
  75 + * @param type 消息类型:1=订单 2=系统 null=全部
  76 + */
  77 + @PostMapping("/read-all")
  78 + public Result<Void> markAllRead(@RequestParam(required = false) Integer type, HttpServletRequest request) {
  79 + Long riderId = (Long) request.getAttribute("riderId");
  80 + messageService.markAllRead(riderId, type);
  81 + return Result.success();
  82 + }
  83 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderMessage.java 0 → 100644
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +/**
  9 + * 骑手消息表
  10 + */
  11 +@Data
  12 +@TableName("rider_message")
  13 +public class RiderMessage {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 城市ID(多租户隔离) */
  19 + private Long cityId;
  20 +
  21 + /** 骑手ID,0表示全员消息 */
  22 + private Long riderId;
  23 +
  24 + /** 消息类型:1=订单消息 2=系统通知 */
  25 + private Integer type;
  26 +
  27 + /** 消息标题 */
  28 + private String title;
  29 +
  30 + /** 消息内容 */
  31 + private String content;
  32 +
  33 + /** 业务类型:order_assigned/order_timeout/system_notice */
  34 + private String bizType;
  35 +
  36 + /** 关联业务ID(如订单ID) */
  37 + private Long bizId;
  38 +
  39 + /** 扩展数据(JSON格式) */
  40 + private String extraData;
  41 +
  42 + /** 已读状态:0=未读 1=已读 */
  43 + private Integer isRead;
  44 +
  45 + /** 创建时间 */
  46 + private Long createTime;
  47 +
  48 + /** 阅读时间 */
  49 + private Long readTime;
  50 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderMessageTemplate.java 0 → 100644
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +/**
  9 + * 消息模板表
  10 + */
  11 +@Data
  12 +@TableName("rider_message_template")
  13 +public class RiderMessageTemplate {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 业务类型标识 */
  19 + private String bizType;
  20 +
  21 + /** 消息类型:1=订单 2=系统 */
  22 + private Integer type;
  23 +
  24 + /** 标题模板,支持 #{key} 占位符 */
  25 + private String titleTemplate;
  26 +
  27 + /** 内容模板,支持 #{key} 占位符 */
  28 + private String contentTemplate;
  29 +
  30 + /** 是否实时推送:0=否 1=是 */
  31 + private Integer isPush;
  32 +
  33 + /** 状态:0=禁用 1=启用 */
  34 + private Integer status;
  35 +
  36 + /** 创建时间 */
  37 + private Long createTime;
  38 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderMessageMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderMessage;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderMessageMapper extends BaseMapper<RiderMessage> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderMessageTemplateMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderMessageTemplate;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderMessageTemplateMapper extends BaseMapper<RiderMessageTemplate> {
  9 +}
... ...
src/main/java/com/diligrp/rider/service/AdminMessageService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.RiderMessageTemplate;
  4 +
  5 +import java.util.List;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * 管理端消息服务
  10 + */
  11 +public interface AdminMessageService {
  12 +
  13 + /**
  14 + * 发送消息给指定骑手
  15 + *
  16 + * @param cityId 城市ID
  17 + * @param riderId 骑手ID
  18 + * @param type 消息类型
  19 + * @param title 标题
  20 + * @param content 内容
  21 + * @param bizType 业务类型
  22 + * @param bizId 业务ID
  23 + */
  24 + void sendToRider(Long cityId, Long riderId, Integer type, String title, String content, String bizType, Long bizId);
  25 +
  26 + /**
  27 + * 群发消息(城市内所有骑手)
  28 + *
  29 + * @param cityId 城市ID
  30 + * @param type 消息类型
  31 + * @param title 标题
  32 + * @param content 内容
  33 + */
  34 + void broadcastToCity(Long cityId, Integer type, String title, String content);
  35 +
  36 + /**
  37 + * 查询消息列表(管理端)
  38 + *
  39 + * @param cityId 城市ID
  40 + * @param riderId 骑手ID(可选)
  41 + * @param type 消息类型(可选)
  42 + * @param page 页码
  43 + * @return 分页数据
  44 + */
  45 + Map<String, Object> listMessages(Long cityId, Long riderId, Integer type, Integer page);
  46 +
  47 + /**
  48 + * 获取模板列表
  49 + *
  50 + * @return 模板列表
  51 + */
  52 + List<RiderMessageTemplate> listTemplates();
  53 +
  54 + /**
  55 + * 根据模板发送消息(供业务代码调用)
  56 + *
  57 + * @param cityId 城市ID
  58 + * @param riderId 骑手ID
  59 + * @param bizType 业务类型
  60 + * @param params 模板参数
  61 + */
  62 + void sendByTemplate(Long cityId, Long riderId, String bizType, Map<String, Object> params);
  63 +}
... ...
src/main/java/com/diligrp/rider/service/RiderMessageService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import java.util.Map;
  4 +
  5 +/**
  6 + * 骑手端消息服务
  7 + */
  8 +public interface RiderMessageService {
  9 +
  10 + /**
  11 + * 获取骑手消息列表(分页)
  12 + *
  13 + * @param riderId 骑手ID
  14 + * @param type 消息类型:1=订单 2=系统 null=全部
  15 + * @param page 页码
  16 + * @param pageSize 每页数量
  17 + * @return 分页数据
  18 + */
  19 + Map<String, Object> listByRider(Long riderId, Integer type, Integer page, Integer pageSize);
  20 +
  21 + /**
  22 + * 获取未读消息数
  23 + *
  24 + * @param riderId 骑手ID
  25 + * @return {total: 总数, order: 订单消息数, system: 系统消息数}
  26 + */
  27 + Map<String, Integer> getUnreadCount(Long riderId);
  28 +
  29 + /**
  30 + * 标记单条消息为已读
  31 + *
  32 + * @param riderId 骑手ID
  33 + * @param messageId 消息ID
  34 + */
  35 + void markRead(Long riderId, Long messageId);
  36 +
  37 + /**
  38 + * 批量标记已读
  39 + *
  40 + * @param riderId 骑手ID
  41 + * @param messageIds 消息ID列表
  42 + */
  43 + void markReadBatch(Long riderId, java.util.List<Long> messageIds);
  44 +
  45 + /**
  46 + * 全部标记已读
  47 + *
  48 + * @param riderId 骑手ID
  49 + * @param type 消息类型:1=订单 2=系统 null=全部
  50 + */
  51 + void markAllRead(Long riderId, Integer type);
  52 +}
... ...
src/main/java/com/diligrp/rider/service/impl/AdminMessageServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  5 +import com.diligrp.rider.entity.Rider;
  6 +import com.diligrp.rider.entity.RiderMessage;
  7 +import com.diligrp.rider.entity.RiderMessageTemplate;
  8 +import com.diligrp.rider.mapper.RiderMapper;
  9 +import com.diligrp.rider.mapper.RiderMessageMapper;
  10 +import com.diligrp.rider.mapper.RiderMessageTemplateMapper;
  11 +import com.diligrp.rider.service.AdminMessageService;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Service;
  15 +import org.springframework.transaction.annotation.Transactional;
  16 +
  17 +import java.util.ArrayList;
  18 +import java.util.HashMap;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +
  22 +@Slf4j
  23 +@Service
  24 +@RequiredArgsConstructor
  25 +public class AdminMessageServiceImpl implements AdminMessageService {
  26 +
  27 + private final RiderMessageMapper messageMapper;
  28 + private final RiderMessageTemplateMapper templateMapper;
  29 + private final RiderMapper riderMapper;
  30 +
  31 + private static final int PAGE_SIZE = 20;
  32 +
  33 + @Override
  34 + @Transactional
  35 + public void sendToRider(Long cityId, Long riderId, Integer type, String title, String content, String bizType, Long bizId) {
  36 + RiderMessage message = new RiderMessage();
  37 + message.setCityId(cityId);
  38 + message.setRiderId(riderId);
  39 + message.setType(type);
  40 + message.setTitle(title);
  41 + message.setContent(content);
  42 + message.setBizType(bizType != null ? bizType : "");
  43 + message.setBizId(bizId != null ? bizId : 0L);
  44 + message.setIsRead(0);
  45 + message.setCreateTime(System.currentTimeMillis());
  46 + message.setReadTime(0L);
  47 +
  48 + messageMapper.insert(message);
  49 + log.info("发送消息成功,riderId={} messageId={}", riderId, message.getId());
  50 + }
  51 +
  52 + @Override
  53 + @Transactional
  54 + public void broadcastToCity(Long cityId, Integer type, String title, String content) {
  55 + // 查询城市内所有正常状态的骑手
  56 + LambdaQueryWrapper<Rider> wrapper = new LambdaQueryWrapper<>();
  57 + wrapper.eq(Rider::getCityId, cityId)
  58 + .eq(Rider::getStatus, 1); // 只发给正常状态的骑手
  59 + List<Rider> riders = riderMapper.selectList(wrapper);
  60 +
  61 + if (riders.isEmpty()) {
  62 + log.warn("城市内无可用骑手,cityId={}", cityId);
  63 + return;
  64 + }
  65 +
  66 + // 批量插入消息
  67 + List<RiderMessage> messages = new ArrayList<>();
  68 + long now = System.currentTimeMillis();
  69 + for (Rider rider : riders) {
  70 + RiderMessage message = new RiderMessage();
  71 + message.setCityId(cityId);
  72 + message.setRiderId(rider.getId());
  73 + message.setType(type);
  74 + message.setTitle(title);
  75 + message.setContent(content);
  76 + message.setBizType("broadcast");
  77 + message.setBizId(0L);
  78 + message.setIsRead(0);
  79 + message.setCreateTime(now);
  80 + message.setReadTime(0L);
  81 + messages.add(message);
  82 + }
  83 +
  84 + // 批量插入(每次最多1000条)
  85 + int batchSize = 1000;
  86 + for (int i = 0; i < messages.size(); i += batchSize) {
  87 + int end = Math.min(i + batchSize, messages.size());
  88 + List<RiderMessage> batch = messages.subList(i, end);
  89 + batch.forEach(messageMapper::insert);
  90 + }
  91 +
  92 + log.info("群发消息成功,cityId={} 骑手数={}", cityId, riders.size());
  93 + }
  94 +
  95 + @Override
  96 + public Map<String, Object> listMessages(Long cityId, Long riderId, Integer type, Integer page) {
  97 + Page<RiderMessage> pageObj = new Page<>(page, PAGE_SIZE);
  98 + LambdaQueryWrapper<RiderMessage> wrapper = new LambdaQueryWrapper<>();
  99 + wrapper.eq(RiderMessage::getCityId, cityId)
  100 + .eq(riderId != null, RiderMessage::getRiderId, riderId)
  101 + .eq(type != null, RiderMessage::getType, type)
  102 + .orderByDesc(RiderMessage::getCreateTime);
  103 +
  104 + Page<RiderMessage> result = messageMapper.selectPage(pageObj, wrapper);
  105 +
  106 + Map<String, Object> data = new HashMap<>();
  107 + data.put("list", result.getRecords());
  108 + data.put("total", result.getTotal());
  109 + data.put("page", page);
  110 + data.put("pageSize", PAGE_SIZE);
  111 + return data;
  112 + }
  113 +
  114 + @Override
  115 + public List<RiderMessageTemplate> listTemplates() {
  116 + LambdaQueryWrapper<RiderMessageTemplate> wrapper = new LambdaQueryWrapper<>();
  117 + wrapper.eq(RiderMessageTemplate::getStatus, 1)
  118 + .orderByAsc(RiderMessageTemplate::getId);
  119 + return templateMapper.selectList(wrapper);
  120 + }
  121 +
  122 + @Override
  123 + @Transactional
  124 + public void sendByTemplate(Long cityId, Long riderId, String bizType, Map<String, Object> params) {
  125 + // 查询模板
  126 + LambdaQueryWrapper<RiderMessageTemplate> wrapper = new LambdaQueryWrapper<>();
  127 + wrapper.eq(RiderMessageTemplate::getBizType, bizType)
  128 + .eq(RiderMessageTemplate::getStatus, 1);
  129 + RiderMessageTemplate template = templateMapper.selectOne(wrapper);
  130 +
  131 + if (template == null) {
  132 + log.warn("消息模板不存在,bizType={}", bizType);
  133 + return;
  134 + }
  135 +
  136 + // 替换占位符
  137 + String title = replacePlaceholders(template.getTitleTemplate(), params);
  138 + String content = replacePlaceholders(template.getContentTemplate(), params);
  139 +
  140 + // 发送消息
  141 + Long bizId = params.get("bizId") != null ? Long.valueOf(params.get("bizId").toString()) : 0L;
  142 + sendToRider(cityId, riderId, template.getType(), title, content, bizType, bizId);
  143 + }
  144 +
  145 + /**
  146 + * 替换模板占位符
  147 + */
  148 + private String replacePlaceholders(String template, Map<String, Object> params) {
  149 + String result = template;
  150 + for (Map.Entry<String, Object> entry : params.entrySet()) {
  151 + String placeholder = "#{" + entry.getKey() + "}";
  152 + String value = entry.getValue() != null ? entry.getValue().toString() : "";
  153 + result = result.replace(placeholder, value);
  154 + }
  155 + return result;
  156 + }
  157 +}
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java
... ... @@ -17,6 +17,7 @@ import com.diligrp.rider.service.DispatchRuleService;
17 17 import com.diligrp.rider.service.DispatchService;
18 18 import com.diligrp.rider.service.MerchantService;
19 19 import com.diligrp.rider.service.WebhookService;
  20 +import com.diligrp.rider.service.AdminMessageService;
20 21 import com.diligrp.rider.vo.DeliveryFeeResultVO;
21 22 import com.diligrp.rider.vo.DeliveryOrderCreateVO;
22 23 import com.diligrp.rider.vo.DispatchRuleTemplateVO;
... ... @@ -46,6 +47,7 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
46 47 private final DispatchService dispatchService;
47 48 private final WebhookService webhookService;
48 49 private final ObjectMapper objectMapper;
  50 + private final AdminMessageService adminMessageService;
49 51  
50 52 @Override
51 53 @Transactional
... ... @@ -221,6 +223,18 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
221 223 .set(Orders::getStatus, 10));
222 224 order.setStatus(10);
223 225 notifyCallback(order, "order.cancelled");
  226 +
  227 + // 发送消息:订单取消(如果已分配骑手)
  228 + if (order.getRiderId() != null && order.getRiderId() > 0) {
  229 + try {
  230 + Map<String, Object> params = new HashMap<>();
  231 + params.put("orderNo", order.getOrderNo());
  232 + params.put("bizId", order.getId());
  233 + adminMessageService.sendByTemplate(order.getCityId(), order.getRiderId(), "order_cancelled", params);
  234 + } catch (Exception e) {
  235 + log.error("发送订单取消消息失败,orderId={} riderId={}", order.getId(), order.getRiderId(), e);
  236 + }
  237 + }
224 238 }
225 239  
226 240 private DeliveryFeeResultVO buildFeeVO(Orders order) {
... ...
src/main/java/com/diligrp/rider/service/impl/RiderMessageServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6 +import com.diligrp.rider.entity.RiderMessage;
  7 +import com.diligrp.rider.mapper.RiderMessageMapper;
  8 +import com.diligrp.rider.service.RiderMessageService;
  9 +import lombok.RequiredArgsConstructor;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.stereotype.Service;
  12 +import org.springframework.transaction.annotation.Transactional;
  13 +
  14 +import java.util.HashMap;
  15 +import java.util.List;
  16 +import java.util.Map;
  17 +
  18 +@Slf4j
  19 +@Service
  20 +@RequiredArgsConstructor
  21 +public class RiderMessageServiceImpl implements RiderMessageService {
  22 +
  23 + private final RiderMessageMapper messageMapper;
  24 +
  25 + @Override
  26 + public Map<String, Object> listByRider(Long riderId, Integer type, Integer page, Integer pageSize) {
  27 + Page<RiderMessage> pageObj = new Page<>(page, pageSize);
  28 + LambdaQueryWrapper<RiderMessage> wrapper = new LambdaQueryWrapper<>();
  29 + wrapper.eq(RiderMessage::getRiderId, riderId)
  30 + .eq(type != null, RiderMessage::getType, type)
  31 + .orderByDesc(RiderMessage::getCreateTime);
  32 +
  33 + Page<RiderMessage> result = messageMapper.selectPage(pageObj, wrapper);
  34 +
  35 + Map<String, Object> data = new HashMap<>();
  36 + data.put("list", result.getRecords());
  37 + data.put("total", result.getTotal());
  38 + data.put("page", page);
  39 + data.put("pageSize", pageSize);
  40 + return data;
  41 + }
  42 +
  43 + @Override
  44 + public Map<String, Integer> getUnreadCount(Long riderId) {
  45 + LambdaQueryWrapper<RiderMessage> wrapper = new LambdaQueryWrapper<>();
  46 + wrapper.eq(RiderMessage::getRiderId, riderId)
  47 + .eq(RiderMessage::getIsRead, 0);
  48 +
  49 + Long total = messageMapper.selectCount(wrapper);
  50 +
  51 + wrapper.eq(RiderMessage::getType, 1);
  52 + Long orderCount = messageMapper.selectCount(wrapper);
  53 +
  54 + wrapper.clear();
  55 + wrapper.eq(RiderMessage::getRiderId, riderId)
  56 + .eq(RiderMessage::getIsRead, 0)
  57 + .eq(RiderMessage::getType, 2);
  58 + Long systemCount = messageMapper.selectCount(wrapper);
  59 +
  60 + Map<String, Integer> result = new HashMap<>();
  61 + result.put("total", total.intValue());
  62 + result.put("order", orderCount.intValue());
  63 + result.put("system", systemCount.intValue());
  64 + return result;
  65 + }
  66 +
  67 + @Override
  68 + @Transactional
  69 + public void markRead(Long riderId, Long messageId) {
  70 + RiderMessage message = messageMapper.selectById(messageId);
  71 + if (message != null && message.getRiderId().equals(riderId) && message.getIsRead() == 0) {
  72 + message.setIsRead(1);
  73 + message.setReadTime(System.currentTimeMillis());
  74 + messageMapper.updateById(message);
  75 + }
  76 + }
  77 +
  78 + @Override
  79 + @Transactional
  80 + public void markReadBatch(Long riderId, List<Long> messageIds) {
  81 + if (messageIds == null || messageIds.isEmpty()) {
  82 + return;
  83 + }
  84 +
  85 + LambdaUpdateWrapper<RiderMessage> wrapper = new LambdaUpdateWrapper<>();
  86 + wrapper.eq(RiderMessage::getRiderId, riderId)
  87 + .in(RiderMessage::getId, messageIds)
  88 + .eq(RiderMessage::getIsRead, 0)
  89 + .set(RiderMessage::getIsRead, 1)
  90 + .set(RiderMessage::getReadTime, System.currentTimeMillis());
  91 +
  92 + messageMapper.update(null, wrapper);
  93 + }
  94 +
  95 + @Override
  96 + @Transactional
  97 + public void markAllRead(Long riderId, Integer type) {
  98 + LambdaUpdateWrapper<RiderMessage> wrapper = new LambdaUpdateWrapper<>();
  99 + wrapper.eq(RiderMessage::getRiderId, riderId)
  100 + .eq(type != null, RiderMessage::getType, type)
  101 + .eq(RiderMessage::getIsRead, 0)
  102 + .set(RiderMessage::getIsRead, 1)
  103 + .set(RiderMessage::getReadTime, System.currentTimeMillis());
  104 +
  105 + messageMapper.update(null, wrapper);
  106 + }
  107 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderOrderServiceImpl.java
... ... @@ -12,6 +12,7 @@ import com.diligrp.rider.service.RiderHoldLimitService;
12 12 import com.diligrp.rider.service.RiderLevelService;
13 13 import com.diligrp.rider.service.RiderOrderService;
14 14 import com.diligrp.rider.service.WebhookService;
  15 +import com.diligrp.rider.service.AdminMessageService;
15 16 import com.diligrp.rider.vo.DispatchRuleTemplateVO;
16 17 import com.diligrp.rider.vo.OrderVO;
17 18 import com.diligrp.rider.vo.RiderMonthCountVO;
... ... @@ -44,6 +45,7 @@ public class RiderOrderServiceImpl implements RiderOrderService {
44 45 private final RiderHoldLimitService riderHoldLimitService;
45 46 private final ObjectMapper objectMapper;
46 47 private final WebhookService webhookService;
  48 + private final AdminMessageService adminMessageService;
47 49  
48 50 private static final int PAGE_SIZE = 20;
49 51  
... ... @@ -158,6 +160,16 @@ public class RiderOrderServiceImpl implements RiderOrderService {
158 160 // 通知接入方:骑手已接单
159 161 notifyOrderEvent(orderId, "order.accepted");
160 162  
  163 + // 发送消息:新订单
  164 + try {
  165 + Map<String, Object> params = new HashMap<>();
  166 + params.put("orderNo", order.getOrderNo());
  167 + params.put("bizId", orderId);
  168 + adminMessageService.sendByTemplate(order.getCityId(), riderId, "order_assigned", params);
  169 + } catch (Exception e) {
  170 + log.error("发送新订单消息失败,orderId={} riderId={}", orderId, riderId, e);
  171 + }
  172 +
161 173 // 删除该骑手的拒单记录(抢到了就清掉)
162 174 refuseMapper.delete(new LambdaQueryWrapper<RiderOrderRefuse>()
163 175 .eq(RiderOrderRefuse::getOid, orderId));
... ...
src/main/java/com/diligrp/rider/task/OrderScheduleTask.java
... ... @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
6 6 import com.diligrp.rider.entity.Orders;
7 7 import com.diligrp.rider.mapper.OrdersMapper;
8 8 import com.diligrp.rider.service.WebhookService;
  9 +import com.diligrp.rider.service.AdminMessageService;
9 10 import lombok.RequiredArgsConstructor;
10 11 import lombok.extern.slf4j.Slf4j;
11 12 import org.springframework.scheduling.annotation.EnableScheduling;
... ... @@ -29,6 +30,7 @@ public class OrderScheduleTask {
29 30 private final OrdersMapper ordersMapper;
30 31 private final WebhookService webhookService;
31 32 private final ObjectMapper objectMapper;
  33 + private final AdminMessageService adminMessageService;
32 34  
33 35 /**
34 36 * 超时未接单订单自动取消(30分钟)
... ... @@ -53,6 +55,55 @@ public class OrderScheduleTask {
53 55 log.info("订单超时自动取消 orderId={}", order.getId());
54 56 // 通知接入方
55 57 notifyCancel(order);
  58 + // 发送消息:订单取消(如果已分配骑手)
  59 + if (order.getRiderId() != null && order.getRiderId() > 0) {
  60 + try {
  61 + Map<String, Object> params = new HashMap<>();
  62 + params.put("orderNo", order.getOrderNo());
  63 + params.put("bizId", order.getId());
  64 + adminMessageService.sendByTemplate(order.getCityId(), order.getRiderId(), "order_cancelled", params);
  65 + } catch (Exception e) {
  66 + log.error("发送订单取消消息失败,orderId={} riderId={}", order.getId(), order.getRiderId(), e);
  67 + }
  68 + }
  69 + }
  70 + }
  71 + } catch (Exception e) {
  72 + log.error("超时取消任务异常", e);
  73 + }
  74 + }
  75 +
  76 + /**
  77 + * 订单超时提醒(配送中的订单,距离预计送达时间还有5分钟)
  78 + * 每分钟执行一次
  79 + */
  80 + @Scheduled(fixedDelay = 60_000)
  81 + public void timeoutReminder() {
  82 + try {
  83 + long now = System.currentTimeMillis() / 1000;
  84 + // 查询配送中的订单(状态=4)
  85 + List<Orders> deliveringOrders = ordersMapper.selectList(
  86 + new LambdaQueryWrapper<Orders>()
  87 + .eq(Orders::getStatus, 4)
  88 + .isNotNull(Orders::getRiderId)
  89 + .gt(Orders::getRiderId, 0));
  90 +
  91 + for (Orders order : deliveringOrders) {
  92 + // 简单逻辑:取件后超过30分钟提醒
  93 + if (order.getPickTime() != null && order.getPickTime() > 0) {
  94 + long elapsedMinutes = (now - order.getPickTime()) / 60;
  95 + if (elapsedMinutes >= 30 && elapsedMinutes < 31) {
  96 + // 发送超时提醒消息
  97 + try {
  98 + Map<String, Object> params = new HashMap<>();
  99 + params.put("orderNo", order.getOrderNo());
  100 + params.put("bizId", order.getId());
  101 + adminMessageService.sendByTemplate(order.getCityId(), order.getRiderId(), "order_timeout", params);
  102 + log.info("发送订单超时提醒,orderId={} riderId={}", order.getId(), order.getRiderId());
  103 + } catch (Exception e) {
  104 + log.error("发送订单超时提醒失败,orderId={} riderId={}", order.getId(), order.getRiderId(), e);
  105 + }
  106 + }
56 107 }
57 108 }
58 109 } catch (Exception e) {
... ...
src/main/resources/schema.sql
... ... @@ -538,4 +538,47 @@ CREATE TABLE `dispatch_rule_condition` (
538 538 PRIMARY KEY (`id`),
539 539 UNIQUE KEY `uk_template_type` (`template_id`, `condition_type`),
540 540 KEY `idx_template_order` (`template_id`, `sort_order`)
541   -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='派单优先级条件表';
542 541 \ No newline at end of file
  542 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='派单优先级条件表';
  543 +
  544 +-- 骑手消息表
  545 +CREATE TABLE `rider_message` (
  546 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '消息ID',
  547 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID(多租户隔离)',
  548 + `rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '骑手ID,0表示全员消息',
  549 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型:1=订单消息 2=系统通知',
  550 + `title` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '消息标题',
  551 + `content` TEXT NOT NULL COMMENT '消息内容',
  552 + `biz_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型:order_assigned/order_timeout/system_notice',
  553 + `biz_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联业务ID(如订单ID)',
  554 + `extra_data` TEXT COMMENT '扩展数据(JSON格式)',
  555 + `is_read` TINYINT NOT NULL DEFAULT 0 COMMENT '已读状态:0=未读 1=已读',
  556 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
  557 + `read_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时间',
  558 + PRIMARY KEY (`id`),
  559 + KEY `idx_rider_type` (`rider_id`, `type`, `create_time`),
  560 + KEY `idx_city_create` (`city_id`, `create_time`),
  561 + KEY `idx_biz` (`biz_type`, `biz_id`)
  562 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手消息表';
  563 +
  564 +-- 消息模板表
  565 +CREATE TABLE `rider_message_template` (
  566 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  567 + `biz_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型标识',
  568 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型:1=订单 2=系统',
  569 + `title_template` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '标题模板,支持 #{key} 占位符',
  570 + `content_template` TEXT NOT NULL COMMENT '内容模板,支持 #{key} 占位符',
  571 + `is_push` TINYINT NOT NULL DEFAULT 1 COMMENT '是否实时推送:0=否 1=是',
  572 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
  573 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  574 + PRIMARY KEY (`id`),
  575 + UNIQUE KEY `uk_biz_type` (`biz_type`)
  576 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息模板表';
  577 +
  578 +-- 初始化模板数据
  579 +INSERT INTO `rider_message_template` (`biz_type`, `type`, `title_template`, `content_template`, `is_push`) VALUES
  580 +('order_assigned', 1, '新订单', '您有新的配送订单 #{orderNo},请及时处理', 1),
  581 +('order_timeout', 1, '订单超时提醒', '订单 #{orderNo} 即将超时,请尽快送达', 1),
  582 +('order_cancelled', 1, '订单取消', '订单 #{orderNo} 已被取消', 1),
  583 +('system_notice', 2, '系统通知', '#{content}', 1),
  584 +('level_upgrade', 2, '等级提升', '恭喜您,等级已提升至 #{levelName}', 1),
  585 +('balance_change', 2, '余额变动', '您的余额发生变动,当前余额:#{balance} 元', 0);
543 586 \ No newline at end of file
... ...