liyong
4 小时以前 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package cn.iocoder.yudao.module.im.service.group;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
 
import java.time.LocalDateTime;
import java.util.*;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
 
/**
 * 群成员 Service 实现类
 *
 * @author 芋道源码
 */
@Slf4j
@Service
@Validated
public class ImGroupMemberServiceImpl implements ImGroupMemberService {
 
    @Resource
    private ImGroupMemberMapper groupMemberMapper;
 
    @Resource
    @Lazy // 避免循环依赖
    private ImGroupMessageService groupMessageService;
 
    @Override
    public ImGroupMemberDO getGroupMember(Long id) {
        return groupMemberMapper.selectById(id);
    }
 
    @Override
    public ImGroupMemberDO getGroupMember(Long groupId, Long userId) {
        return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
    }
 
    @Override
    public List<ImGroupMemberDO> getGroupMembers(Long groupId, Collection<Long> userIds) {
        if (CollUtil.isEmpty(userIds)) {
            return Collections.emptyList();
        }
        return groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds);
    }
 
    @Override
    public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) {
        return groupMemberMapper.selectListByGroupId(groupId);
    }
 
    @Override
    public List<ImGroupMemberDO> getActiveGroupMemberListByGroupId(Long groupId) {
        return groupMemberMapper.selectListByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Override
    public List<ImGroupMemberDO> getGroupMemberListByOwnerAndAdmin(Long groupId) {
        return groupMemberMapper.selectListByGroupIdAndStatusAndRoles(groupId, CommonStatusEnum.ENABLE.getStatus(),
                ListUtil.of(ImGroupMemberRoleEnum.OWNER.getRole(), ImGroupMemberRoleEnum.ADMIN.getRole()));
    }
 
    /**
     * 只缓存 userId 列表而非整个 {@link ImGroupMemberDO},理由:
     * <ul>
     *   <li>体积小:500 人群约 4KB,失效/序列化成本低;</li>
     *   <li>失效面窄:仅 {@link #addGroupMember}/{@link #addGroupMembers}/
     *       {@link #removeGroupMember}/{@link #removeGroupMembers}/{@link #removeGroupMembersByGroupId}
     *       这类影响成员集合的写操作需要失效;
     *       {@link #updateGroupMember}(昵称/备注/免打扰)不修改集合成员,不需要失效。</li>
     * </ul>
     */
    @Override
    @Cacheable(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public List<Long> getActiveGroupMemberUserIdsByGroupId(Long groupId) {
        List<ImGroupMemberDO> members = groupMemberMapper.selectListByGroupIdAndStatus(
                groupId, CommonStatusEnum.ENABLE.getStatus());
        return convertList(members, ImGroupMemberDO::getUserId);
    }
 
    @Override
    public List<ImGroupMemberDO> getActiveGroupMemberListByUserId(Long userId) {
        return groupMemberMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Override
    public List<ImGroupMemberDO> getGroupMemberListByUserId(Long userId) {
        return groupMemberMapper.selectListByUserId(userId);
    }
 
    @Override
    public ImGroupMemberDO addGroupMember(Long groupId, Long userId) {
        return addGroupMember(groupId, userId, ImGroupMemberRoleEnum.NORMAL.getRole(), null, null);
    }
 
    @Override
    public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role) {
        return addGroupMember(groupId, userId, role, null, null);
    }
 
    /**
     * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等,
     * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。
     * <p>
     * 重置旧成员行时强制重置 role / addSource / inviterUserId / quitTime / muteEndTime
     */
    @Override
    @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role,
                                          Integer addSource, Long inviterUserId) {
        LocalDateTime now = LocalDateTime.now();
        // 情况一:已存在记录 → 重置或跳过
        ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
        if (exists != null) {
            if (CommonStatusEnum.isDisable(exists.getStatus())) {
                groupMemberMapper.updateRejoinFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now,
                        role, addSource, inviterUserId);
                exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role)
                        .setAddSource(addSource).setInviterUserId(inviterUserId)
                        .setQuitTime(null).setMuteEndTime(null);
            }
            return exists;
        }
        // 情况二:新增成员
        ImGroupMemberDO member = new ImGroupMemberDO()
                .setGroupId(groupId).setUserId(userId)
                .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)
                .setRole(role).setAddSource(addSource).setInviterUserId(inviterUserId);
        try {
            groupMemberMapper.insert(member);
            return member;
        } catch (DuplicateKeyException e) {
            // 并发场景:另一个请求已先一步插入,且其插入的必然是 ENABLE 状态(DISABLE 场景在上方分支已处理),查询返回
            log.warn("[addGroupMember][groupId({}) userId({}) 并发插入冲突,查询返回]", groupId, userId);
            return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
        }
    }
 
    @Override
    public void addGroupMembers(Long groupId, Collection<Long> userIds) {
        addGroupMembers(groupId, userIds, null, null);
    }
 
    @Override
    @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public void addGroupMembers(Long groupId, Collection<Long> userIds, Integer addSource, Long inviterUserId) {
        LocalDateTime now = LocalDateTime.now();
        Integer role = ImGroupMemberRoleEnum.NORMAL.getRole();
        // 1.1 查询已有记录(含已退群的 DISABLE 记录)
        List<ImGroupMemberDO> existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds);
        Map<Long, ImGroupMemberDO> existMap = convertMap(existMembers, ImGroupMemberDO::getUserId);
        // 1.2 分类:已有记录 → UPDATE,新成员 → INSERT
        List<ImGroupMemberDO> inserts = new ArrayList<>();
        List<ImGroupMemberDO> updates = new ArrayList<>();
        for (Long userId : userIds) {
            ImGroupMemberDO exist = existMap.get(userId);
            if (exist == null) {
                inserts.add(new ImGroupMemberDO().setGroupId(groupId).setUserId(userId)
                        .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)
                        .setAddSource(addSource).setInviterUserId(inviterUserId));
            } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) {
                updates.add(new ImGroupMemberDO().setId(exist.getId())
                        .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)
                        .setAddSource(addSource).setInviterUserId(inviterUserId));
            }
        }
 
        // 2.1 先做 update,update 没有并发冲突风险
        if (CollUtil.isNotEmpty(updates)) {
            for (ImGroupMemberDO update : updates) {
                groupMemberMapper.updateRejoinFields(update.getId(), update.getStatus(), update.getJoinTime(),
                        update.getRole(), update.getAddSource(), update.getInviterUserId());
            }
        }
        // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId),
        //     会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。
        if (CollUtil.isNotEmpty(inserts)) {
            try {
                groupMemberMapper.insertBatch(inserts);
            } catch (DuplicateKeyException e) {
                log.warn("[addGroupMembers][groupId({}) userIds({}) 批量插入冲突,降级为逐个处理]", groupId, userIds);
                for (ImGroupMemberDO insert : inserts) {
                    addGroupMember(groupId, insert.getUserId(), role, addSource, inviterUserId);
                }
            }
        }
    }
 
    @Override
    public ImGroupMemberDO validateMemberInGroup(Long groupId, Long userId) {
        ImGroupMemberDO member = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
        if (member == null || CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) {
            throw exception(GROUP_MEMBER_NOT_IN_GROUP);
        }
        return member;
    }
 
    @Override
    public void validateMembersInGroup(Long groupId, Collection<Long> userIds) {
        if (CollUtil.isEmpty(userIds)) {
            return;
        }
        // 一次性拉取目标 userId 的成员记录,仅保留活跃状态
        List<ImGroupMemberDO> members = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds);
        Set<Long> activeUserIds = convertSet(members, ImGroupMemberDO::getUserId,
                member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus()));
        // 任一 userId 不在活跃集合即抛
        for (Long userId : userIds) {
            if (!activeUserIds.contains(userId)) {
                throw exception(GROUP_MEMBER_NOT_IN_GROUP);
            }
        }
    }
 
    @Override
    public int updateGroupMemberRole(Long groupId, Collection<Long> userIds, Integer role) {
        if (CollUtil.isEmpty(userIds)) {
            return 0;
        }
        return groupMemberMapper.updateListByGroupIdAndUserIds(groupId, userIds, new ImGroupMemberDO().setRole(role));
    }
 
    @Override
    public Long getGroupMemberCountByRole(Long groupId, Integer role) {
        return groupMemberMapper.selectCountByGroupIdAndRoleAndStatus(
                groupId, role, CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Override
    public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) {
        Long groupId = updateReqVO.getGroupId();
        // 1. 校验是群的有效成员
        ImGroupMemberDO member = validateMemberInGroup(groupId, userId);
 
        // 2. 更新群成员信息
        ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class)
                .setId(member.getId());
        groupMemberMapper.updateById(updateObj);
 
        // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 在线同步给全员;空串视为「清空昵称」也要同步;与旧值相同跳过
        if (updateReqVO.getDisplayUserName() != null
                && ObjUtil.notEqual(updateReqVO.getDisplayUserName(), member.getDisplayUserName())) {
            groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate(
                    groupId, userId, updateReqVO.getDisplayUserName()));
        }
        // 3.2 silent / groupRemark 是个人字段,仅推自己做多端同步;与旧值都相同跳过
        boolean silentChanged = updateReqVO.getSilent() != null
                && ObjUtil.notEqual(updateReqVO.getSilent(), member.getSilent());
        boolean groupRemarkChanged = updateReqVO.getGroupRemark() != null
                && ObjUtil.notEqual(updateReqVO.getGroupRemark(), member.getGroupRemark());
        if (silentChanged || groupRemarkChanged) {
            groupMessageService.sendGroupMessage(userId, ListUtil.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate(
                    groupId, userId, updateReqVO.getSilent(), updateReqVO.getGroupRemark()));
        }
    }
 
    @Override
    @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public void removeGroupMember(Long groupId, Long userId) {
        // 1. 校验是群的有效成员
        ImGroupMemberDO member = validateMemberInGroup(groupId, userId);
        // 2. 更新为退群状态
        groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId())
                .setStatus(CommonStatusEnum.DISABLE.getStatus())
                .setQuitTime(LocalDateTime.now()));
    }
 
    @Override
    @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public void removeGroupMembers(Long groupId, Collection<Long> userIds) {
        groupMemberMapper.updateByGroupIdAndUserIdsAndStatus(groupId, userIds,
                CommonStatusEnum.ENABLE.getStatus(),
                new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus())
                        .setQuitTime(LocalDateTime.now()));
    }
 
    @Override
    @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId")
    public void removeGroupMembersByGroupId(Long groupId) {
        groupMemberMapper.updateByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus(),
                new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus())
                        .setQuitTime(LocalDateTime.now()));
    }
 
    @Override
    public Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds) {
        return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Override
    public void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime) {
        ImGroupMemberDO member = validateMemberInGroup(groupId, userId);
        if (muteEndTime != null) {
            // 禁言:直接更新到期时间
            groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId()).setMuteEndTime(muteEndTime));
        } else {
            // 取消禁言
            groupMemberMapper.updateMuteEndTimeNull(member.getId());
        }
    }
 
}