Commit ae71b7134c5545d703a940c2d00a79459efa9588

Authored by YunaiV
1 parent bed0a463

✨ feat(im): 初始化群申请 v0.2:第三把 review

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
... ... @@ -74,6 +74,10 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
74 74 READ(2201, "已读", false, false),
75 75  
76 76 // ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
  77 + // TODO @芋艿:FRIEND_REQUEST_* 与 GROUP_REQUEST_* 都是 persistent=false 的 SysMsg,离线 pull 拉不到,
  78 + // 目前可能丢失「实时 toast 提醒」体验(业务状态不丢,前端上线 fetch{Friend,Group}RequestList 能补回来);
  79 + // 未来思考下怎么优化:候选方案 1)改 persistent=true 入私聊消息流 + 让客户端按 type 自渲染;
  80 + // 2)服务端补一个「未读通知拉取」接口给前端冷启动调用。
77 81 /**
78 82 * 对应 OpenIM:FriendApplicationApprovedNotification 1201
79 83 * 对应自己的类:FriendRequestApprovedNotification
... ... @@ -148,7 +152,7 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
148 152 GROUP_INFO_UPDATE(1502, "群信息变更", true, false),
149 153 /**
150 154 * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503)
151   - * 对应自己的类:GroupRequestNotification
  155 + * 对应自己的类:GroupRequestReceivedNotification
152 156 * 场景:用户申请加群 / 普通成员邀请待审批,定向私聊推送给群主 + 全部管理员(多端同步);不入群消息流
153 157 */
154 158 GROUP_REQUEST_RECEIVED(1503, "收到新的入群申请", false, false),
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.hutool.core.bean.copier.CopyOptions;
5 3 import cn.hutool.core.util.ObjUtil;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
6 5 import cn.hutool.extra.spring.SpringUtil;
7 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
8 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
... ... @@ -89,18 +88,22 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
89 88 }
90 89  
91 90 // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹
  91 + // null 字段(handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null
  92 + // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】
92 93 ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId);
93 94 if (request != null) {
94   - // TODO @AI:相同的放在一行里;然后 null 直接这样更新是清理不掉的;看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?
95   - ImFriendRequestDO updateObj = new ImFriendRequestDO()
96   - .setApplyContent(reqVO.getApplyContent())
97   - .setDisplayName(reqVO.getDisplayName())
  95 + friendRequestMapper.update(null, new LambdaUpdateWrapper<ImFriendRequestDO>()
  96 + .eq(ImFriendRequestDO::getId, request.getId())
  97 + .set(ImFriendRequestDO::getApplyContent, reqVO.getApplyContent())
  98 + .set(ImFriendRequestDO::getDisplayName, reqVO.getDisplayName())
  99 + .set(ImFriendRequestDO::getAddSource, reqVO.getAddSource())
  100 + .set(ImFriendRequestDO::getHandleResult, ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
  101 + .set(ImFriendRequestDO::getHandleContent, null)
  102 + .set(ImFriendRequestDO::getHandleTime, null));
  103 + request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName())
98 104 .setAddSource(reqVO.getAddSource())
99 105 .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
100 106 .setHandleContent(null).setHandleTime(null);
101   - updateObj.setId(request.getId());
102   - friendRequestMapper.updateById(updateObj);
103   - BeanUtil.copyProperties(updateObj, request, CopyOptions.create().setIgnoreNullValue(true));
104 107 } else {
105 108 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
106 109 .setFromUserId(fromUserId).setToUserId(toUserId)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.group;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.hutool.core.bean.copier.CopyOptions;
5 3 import cn.hutool.core.collection.CollUtil;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
6 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -21,7 +20,7 @@ import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
21 20 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
22 21 import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.BaseGroupNotification;
23 22 import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestApprovedNotification;
24   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestNotification;
  23 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestReceivedNotification;
25 24 import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestRejectedNotification;
26 25 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
27 26 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
... ... @@ -36,6 +35,7 @@ import java.time.LocalDateTime;
36 35 import java.util.*;
37 36  
38 37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  38 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
39 39 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
40 40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
41 41  
... ... @@ -95,17 +95,23 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
95 95  
96 96 // 3. 情况二:群开启了审批;upsert 一条「主动申请」记录,inviterUserId=null
97 97 // 已有记录(无论之前是未处理 / 已同意 / 已拒绝),覆盖申请理由 + 来源 + 重置为未处理 + 清空旧处理痕迹
  98 + // null 字段(inviterUserId / handleUserId / handleContent / handleTime)走 LambdaUpdateWrapper.set 显式清空,updateById 默认会忽略 null
  99 + // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】
98 100 ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId);
99 101 if (request != null) {
100   - // TODO @AI:相同的放在一行里;然后 null 直接这样更新是清理不掉的;看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?
101   - ImGroupRequestDO updateObj = new ImGroupRequestDO()
102   - .setInviterUserId(null)
103   - .setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource())
  102 + groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()
  103 + .eq(ImGroupRequestDO::getId, request.getId())
  104 + .set(ImGroupRequestDO::getApplyContent, reqVO.getApplyContent())
  105 + .set(ImGroupRequestDO::getAddSource, reqVO.getAddSource())
  106 + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  107 + .set(ImGroupRequestDO::getInviterUserId, null)
  108 + .set(ImGroupRequestDO::getHandleUserId, null)
  109 + .set(ImGroupRequestDO::getHandleContent, null)
  110 + .set(ImGroupRequestDO::getHandleTime, null));
  111 + request.setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource())
104 112 .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
105   - .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
106   - updateObj.setId(request.getId());
107   - groupRequestMapper.updateById(updateObj);
108   - BeanUtil.copyProperties(updateObj, request, CopyOptions.create().setIgnoreNullValue(true));
  113 + .setInviterUserId(null).setHandleUserId(null)
  114 + .setHandleContent(null).setHandleTime(null);
109 115 } else {
110 116 request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class)
111 117 .setUserId(userId).setInviterUserId(null)
... ... @@ -115,9 +121,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
115 121  
116 122 // 4. 1503 私聊定向推群主 + 全部管理员(多端同步);payload 携带申请方昵称 / 头像
117 123 AdminUserRespDTO applyUser = adminUserApi.getUser(userId);
118   - GroupRequestNotification payload = buildRequestNotification(group, request, applyUser);
119   - for (Long receiverUserId : listOwnerAndAdminUserIds(group)) {
120   - // TODO @AI:能不能发群的,只发给群管理员;就是 groupMessageService;只是不 persist 呀。
  124 + GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
  125 + for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) {
121 126 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
122 127 ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), userId, receiverUserId, payload));
123 128 }
... ... @@ -150,13 +155,9 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
150 155 ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId());
151 156  
152 157 // 5.1 1505 私聊推送给申请人 + 群主 + 全部管理员(每端单推)
153   - // TODO @AI:同类属性,放在一行,减少行占用;其它位置也是!
154   - GroupRequestApprovedNotification payload = (GroupRequestApprovedNotification)
155   - new GroupRequestApprovedNotification()
156   - .setRequestId(request.getId())
157   - .setGroupId(request.getGroupId())
158   - .setUserId(request.getUserId())
159   - .setOperatorUserId(userId);
  158 + GroupRequestApprovedNotification payload = (GroupRequestApprovedNotification) new GroupRequestApprovedNotification()
  159 + .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId())
  160 + .setOperatorUserId(userId);
160 161 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
161 162 ImMessageTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId);
162 163 // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入
... ... @@ -189,13 +190,9 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
189 190 }
190 191  
191 192 // 3. 1506 私聊推送给申请人 + 群主 + 全部管理员
192   - GroupRequestRejectedNotification payload = (GroupRequestRejectedNotification)
193   - new GroupRequestRejectedNotification()
194   - .setRequestId(request.getId())
195   - .setGroupId(request.getGroupId())
196   - .setUserId(request.getUserId())
197   - .setHandleContent(handleContent)
198   - .setOperatorUserId(userId);
  193 + GroupRequestRejectedNotification payload = (GroupRequestRejectedNotification) new GroupRequestRejectedNotification()
  194 + .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId())
  195 + .setHandleContent(handleContent).setOperatorUserId(userId);
199 196 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
200 197 ImMessageTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId);
201 198 }
... ... @@ -209,17 +206,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
209 206 ImGroupDO group = groupService.validateGroupExists(groupId);
210 207  
211 208 // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录
212   - // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id + 重置 handle_result + 清空处理痕迹
  209 + // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id + 重置 handle_result + 清空旧处理痕迹(走 LambdaUpdateWrapper.set 显式置 null)
213 210 List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size());
  211 + // TODO @AI:看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?【主要希望 service 不要出现 mapper 相关的类】
214 212 for (Long invitedUserId : invitedUserIds) {
215 213 ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId);
216 214 if (existing != null) {
217   - ImGroupRequestDO updateObj = new ImGroupRequestDO()
218   - .setInviterUserId(inviterUserId)
219   - .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
220   - .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
221   - updateObj.setId(existing.getId());
222   - groupRequestMapper.updateById(updateObj);
  215 + groupRequestMapper.update(null, new LambdaUpdateWrapper<ImGroupRequestDO>()
  216 + .eq(ImGroupRequestDO::getId, existing.getId())
  217 + .set(ImGroupRequestDO::getInviterUserId, inviterUserId)
  218 + .set(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  219 + .set(ImGroupRequestDO::getHandleUserId, null)
  220 + .set(ImGroupRequestDO::getHandleContent, null)
  221 + .set(ImGroupRequestDO::getHandleTime, null));
223 222 existing.setInviterUserId(inviterUserId)
224 223 .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
225 224 .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
... ... @@ -235,11 +234,10 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
235 234  
236 235 // 2. 推 1503 给群主 + 全部管理员;多端同步;每条申请单独推一帧
237 236 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(invitedUserIds);
238   - List<Long> ownerAndAdmins = listOwnerAndAdminUserIds(group);
  237 + List<Long> ownerAndAdmins = getGroupMemberListByOwnerAndAdminUserIds(group);
239 238 for (ImGroupRequestDO request : requests) {
240 239 AdminUserRespDTO applyUser = userMap.get(request.getUserId());
241   - // TODO @AI:GroupRequestNotification 是不是要调整下命名?因为它是 RECEIVED 的,不然显得太公用了。
242   - GroupRequestNotification payload = buildRequestNotification(group, request, applyUser);
  240 + GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
243 241 for (Long receiverUserId : ownerAndAdmins) {
244 242 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
245 243 ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), inviterUserId, receiverUserId, payload));
... ... @@ -298,18 +296,14 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
298 296 /**
299 297 * 构建 1503 通知 payload;聚合申请方昵称 / 头像供前端直接渲染
300 298 */
301   - private GroupRequestNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request,
302   - AdminUserRespDTO applyUser) {
303   - // TODO @AI:看看能不能链式调用;同类属性,相同行;
304   - GroupRequestNotification payload = (GroupRequestNotification) new GroupRequestNotification()
305   - .setRequestId(request.getId())
306   - .setGroupId(group.getId())
307   - .setUserId(request.getUserId())
  299 + private GroupRequestReceivedNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request,
  300 + AdminUserRespDTO applyUser) {
  301 + Long operatorUserId = request.getInviterUserId() != null ? request.getInviterUserId() : request.getUserId();
  302 + GroupRequestReceivedNotification payload = (GroupRequestReceivedNotification) new GroupRequestReceivedNotification()
  303 + .setRequestId(request.getId()).setGroupId(group.getId()).setUserId(request.getUserId())
308 304 .setInviterUserId(request.getInviterUserId())
309   - .setApplyContent(request.getApplyContent())
310   - .setAddSource(request.getAddSource())
311   - .setOperatorUserId(request.getInviterUserId() != null
312   - ? request.getInviterUserId() : request.getUserId());
  305 + .setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource())
  306 + .setOperatorUserId(operatorUserId);
313 307 if (applyUser != null) {
314 308 payload.setUserNickname(applyUser.getNickname()).setUserAvatar(applyUser.getAvatar());
315 309 }
... ... @@ -325,7 +319,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
325 319 if (group == null) {
326 320 return;
327 321 }
328   - Set<Long> receivers = new LinkedHashSet<>(listOwnerAndAdminUserIds(group));
  322 + Set<Long> receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group));
329 323 receivers.add(applicantUserId);
330 324 for (Long receiverUserId : receivers) {
331 325 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
... ... @@ -334,16 +328,14 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
334 328 }
335 329  
336 330 /**
337   - * 列出群主 + 全部管理员的用户编号;以 group.ownerUserId 兜底,避免 group_member 表里 owner 角色记录异常缺失
  331 + * 列出群主 + 全部管理员的用户编号
  332 + *
  333 + * @param group 群信息
  334 + * @return 群主 + 全部管理员的用户编号列表
338 335 */
339   - // TODO @AI:有点过度设计了,直接 convertset 群成员即可!
340   - private List<Long> listOwnerAndAdminUserIds(ImGroupDO group) {
341   - Set<Long> set = new LinkedHashSet<>();
342   - set.add(group.getOwnerUserId());
343   - for (ImGroupMemberDO member : groupMemberService.getGroupMemberListByOwnerAndAdmin(group.getId())) {
344   - set.add(member.getUserId());
345   - }
346   - return new ArrayList<>(set);
  336 + private List<Long> getGroupMemberListByOwnerAndAdminUserIds(ImGroupDO group) {
  337 + return convertList(groupMemberService.getGroupMemberListByOwnerAndAdmin(group.getId()),
  338 + ImGroupMemberDO::getUserId);
347 339 }
348 340  
349 341 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestReceivedNotification.java
... ... @@ -10,7 +10,7 @@ import lombok.EqualsAndHashCode;
10 10 */
11 11 @EqualsAndHashCode(callSuper = true)
12 12 @Data
13   -public class GroupRequestNotification extends BaseGroupNotification {
  13 +public class GroupRequestReceivedNotification extends BaseGroupNotification {
14 14  
15 15 /**
16 16 * 申请记录编号
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -194,5 +194,6 @@ CREATE TABLE IF NOT EXISTS &quot;im_face_user_item&quot; (
194 194 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
195 195 "deleted" bit NOT NULL DEFAULT FALSE,
196 196 "tenant_id" bigint NOT NULL DEFAULT 0,
197   - PRIMARY KEY ("id")
  197 + PRIMARY KEY ("id"),
  198 + CONSTRAINT "uk_im_face_user_item_user_url" UNIQUE ("user_id", "url", "tenant_id")
198 199 ) COMMENT 'IM 用户私有表情表';
... ...