Commit c90ac42ac06be2726cee3a26b789cd8c09b7a69f
1 parent
0fc95c77
新增消息中心
Showing
14 changed files
with
781 additions
and
1 deletions
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
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 | ... | ... |