package cn.iocoder.yudao.module.im.service.conversation; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; import cn.iocoder.yudao.module.im.dal.mysql.conversation.ImConversationReadMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * IM 会话读位置 Service 实现类 * * @author 芋道源码 */ @Service @Slf4j public class ImConversationReadServiceImpl implements ImConversationReadService { @Resource private ImConversationReadMapper conversationReadMapper; @Override public boolean updateConversationReadPosition(Long userId, Integer conversationType, Long conversationId, Long readMessageId) { LocalDateTime now = LocalDateTime.now(); // 1. 不存在则插入;并发下唯一键冲突,降级为回查 + CAS 更新 ImConversationReadDO existing = conversationReadMapper.selectByUserIdAndConversation( userId, conversationType, conversationId); if (existing == null) { try { conversationReadMapper.insert(new ImConversationReadDO().setUserId(userId) .setConversationType(conversationType).setTargetId(conversationId) .setMessageId(readMessageId).setReadTime(now)); return true; } catch (DuplicateKeyException e) { log.warn("[updateConversationReadPosition][userId({}) type({}) conversationId({}) 并发插入冲突,回查更新]", userId, conversationType, conversationId); existing = conversationReadMapper.selectByUserIdAndConversation(userId, conversationType, conversationId); } } if (existing == null) { return false; } // 2. CAS 单调更新:mapper 内 WHERE message_id < ? 保证乱序 / 并发不回退;影响行数 > 0 即读位置前进 return conversationReadMapper.updateReadMessageIdToLarger(existing.getId(), readMessageId, now) > 0; } @Override public Long getConversationReadMessageId(Long userId, Integer conversationType, Long conversationId) { ImConversationReadDO read = conversationReadMapper.selectByUserIdAndConversation( userId, conversationType, conversationId); return read != null ? read.getMessageId() : null; } @Override public Map getUserReadMessageIdMap(Integer conversationType, Long conversationId) { return convertMap(conversationReadMapper.selectListByConversation(conversationType, conversationId), ImConversationReadDO::getUserId, ImConversationReadDO::getMessageId); } @Override public Map getConversationReadMessageIdMap(Long userId, Integer conversationType, Collection conversationIds) { if (CollUtil.isEmpty(conversationIds)) { return Collections.emptyMap(); } List list = conversationReadMapper.selectListByUserIdAndConversations( userId, conversationType, conversationIds); return convertMap(list, ImConversationReadDO::getTargetId, ImConversationReadDO::getMessageId); } @Override public List pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { return conversationReadMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); } }