liyong
2 小时以前 7a23c450f3ac85de7dca1b908de273ff636ce218
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package cn.iocoder.yudao.module.im.service.message;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessagePageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessageSendReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
import cn.iocoder.yudao.module.im.dal.mysql.message.ImChannelMessageMapper;
import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImChannelMessageNotification;
import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
import cn.iocoder.yudao.module.im.dal.dataobject.message.content.MaterialMessage;
import jakarta.annotation.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.IM_CHANNEL_MESSAGE_NOT_EXISTS;
 
/**
 * IM 频道消息 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
public class ImChannelMessageServiceImpl implements ImChannelMessageService {
 
    @Resource
    private ImChannelMessageMapper channelMessageMapper;
 
    @Resource
    private ImChannelMaterialService channelMaterialService;
    @Resource
    private ImWebSocketService webSocketService;
 
    @Resource
    private ImConversationReadService conversationReadService;
 
    // ==================== 用户端 ====================
 
    @Override
    public List<ImChannelMessageDO> pullChannelMessageList(Long userId, Long minId, Integer size) {
        return channelMessageMapper.selectListByUserAndMinId(userId, minId, size);
    }
 
    @Override
    public Map<Long, Long> getChannelReadMaxMessageIdMap(Long userId, Collection<Long> channelIds) {
        return conversationReadService.getConversationReadMessageIdMap(
                userId, ImConversationTypeEnum.CHANNEL.getType(), channelIds);
    }
 
    @Override
    public void readChannelMessages(Long userId, Long channelId, Long messageId) {
        Assert.notNull(channelId, "频道编号不能为空");
        Assert.notNull(messageId, "已读消息编号不能为空");
        // 1.1 校验消息真实存在且属于该频道,避免未来 / 伪造 messageId 污染读位置
        ImChannelMessageDO message = channelMessageMapper.selectById(messageId);
        if (message == null || ObjUtil.notEqual(message.getChannelId(), channelId)) {
            return;
        }
        // 1.2 定向消息校验对当前用户可见(receiver_user_ids 为空表示全员可见)
        if (CollUtil.isNotEmpty(message.getReceiverUserIds()) && !message.getReceiverUserIds().contains(userId)) {
            return;
        }
 
        // 2. 更新频道已读位置;读位置未前进则不推
        boolean advanced = conversationReadService.updateConversationReadPosition(
                userId, ImConversationTypeEnum.CHANNEL.getType(), channelId, messageId);
        if (!advanced) {
            return;
        }
 
        // 3. 异步推 READ 事件给自己多端同步
        getSelf().readChannelMessageEvent(userId, channelId, messageId);
    }
 
    /**
     * 发送频道已读 READ 事件给自己其他终端;频道无「给发送方刷回执」概念,不广播
     */
    @Async
    public void readChannelMessageEvent(Long userId, Long channelId, Long readId) {
        webSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.CHANNEL.getType(),
                ImContentTypeEnum.READ.getType(), ImMessageReadNotification.ofChannel(channelId, readId));
    }
 
    private ImChannelMessageServiceImpl getSelf() {
        return SpringUtil.getBean(getClass());
    }
 
    // ==================== 管理后台 ====================
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long sendMessage(ImChannelMessageSendReqVO reqVO) {
        // 1. 校验素材存在
        ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(reqVO.getMaterialId());
 
        // 2.1 组装 payload(不带富文本正文);字段同名直接 BeanUtils 拷贝,materialId 单独 set 以兼容转发场景
        MaterialMessage payload = BeanUtils.toBean(material, MaterialMessage.class).setMaterialId(material.getId());
        String payloadJson = JsonUtils.toJsonString(payload);
        // 2.2 落库 1 行 message;reqVO 同名字段(materialId / receiverUserIds)自动拷贝,剩余字段补 set
        ImChannelMessageDO message = BeanUtils.toBean(reqVO, ImChannelMessageDO.class).setChannelId(material.getChannelId())
                .setType(ImContentTypeEnum.MATERIAL.getType()).setContent(payloadJson).setSendTime(LocalDateTime.now());
        channelMessageMapper.insert(message);
 
        // 3. 异步推 WebSocket:指定用户走点对点;全员(receiverUserIds 为空)走广播
        ImChannelMessageNotification dto = ImChannelMessageNotification.ofSend(message);
        if (CollUtil.isNotEmpty(reqVO.getReceiverUserIds())) {
            webSocketService.sendNotificationAsync(reqVO.getReceiverUserIds(), ImConversationTypeEnum.CHANNEL.getType(),
                    dto.getType(), dto);
        } else {
            webSocketService.broadcastNotificationAsync(ImConversationTypeEnum.CHANNEL.getType(), dto.getType(), dto);
        }
        return message.getId();
    }
 
    @Override
    public PageResult<ImChannelMessageDO> getMessagePage(ImChannelMessagePageReqVO reqVO) {
        return channelMessageMapper.selectPage(reqVO);
    }
 
    @Override
    public void deleteMessage(Long id) {
        if (channelMessageMapper.selectById(id) == null) {
            throw exception(IM_CHANNEL_MESSAGE_NOT_EXISTS);
        }
        channelMessageMapper.deleteById(id);
    }
 
}