✨ feat(im):实现 1209 FRIEND_INFO_UPDATED 推送(system 发 AdminUserProfileUpdateMessage,IM Consumer 监听后批量推好友多端)

This commit is contained in:
YunaiV
2026-05-04 18:23:58 +08:00
parent 2804e5ed10
commit b0ca5b7550
6 changed files with 149 additions and 6 deletions
@@ -80,7 +80,7 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
* 对应 OpenIM:FriendInfoUpdatedNotification 1209
* 对应自己的类:FriendInfoUpdatedNotification
* 场景:B 改了昵称 / 头像后,推给 B 的所有好友
* TODO @AI:未实现;待 system 模块改昵称 / 头像时回调 IM 模块批量推此事件,前端 dispatcher 已就绪
* 触发:system 模块发 AdminUserProfileUpdateMessage,IM 消费者 AdminUserProfileUpdateConsumer 批量推此通知
*/
FRIEND_INFO_UPDATED(1209, "好友资料变更", false, false),
/**
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.im.mq.consumer.friend;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendInfoUpdatedNotification;
import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 监听 system 模块的 {@link AdminUserProfileUpdateMessage} 消息,向「资料被改的人」的所有好友推送 FRIEND_INFO_UPDATED 通知
*
* @author 芋道源码
*/
@Slf4j
@Component
public class AdminUserProfileUpdateConsumer {
@Resource
private ImFriendService friendService;
@Resource
private ImWebSocketService websocketService;
@EventListener
@Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步
public void onMessage(AdminUserProfileUpdateMessage message) {
Long userId = message.getUserId();
// 1. 找当前用户的所有有效好友(DISABLE 已删的不推)
List<ImFriendDO> friends = friendService.getFriendList(userId);
if (friends.isEmpty()) {
return;
}
// 2. 给每个好友的多端推 FRIEND_INFO_UPDATED;payload 里 operatorUserId / friendUserId 都是「资料被改的人」
for (ImFriendDO friend : friends) {
if (CommonStatusEnum.isDisable(friend.getStatus())) {
continue;
}
FriendInfoUpdatedNotification payload = (FriendInfoUpdatedNotification) new FriendInfoUpdatedNotification()
.setOperatorUserId(userId).setFriendUserId(userId);
websocketService.sendPrivateMessageAsync(friend.getFriendUserId(), ImPrivateMessageDTO.ofFriendNotification(
ImMessageTypeEnum.FRIEND_INFO_UPDATED.getType(), userId, friend.getFriendUserId(), payload));
}
}
}
@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
import lombok.Data;
// TODO @AI:1209 暂未实现;好友信息在 system 模块,需要 system 改昵称 / 头像后发 ApplicationEvent,IM 模块监听后回调发送此通知
/**
* 好友资料变更通知(对端改了昵称 / 头像)
* <p>
* 推送给资料被改的人的所有好友;前端清缓存 + 重拉资料
* 由 AdminUserProfileUpdateConsumer 监听 system 模块的 AdminUserProfileUpdateMessage 后,批量推送给资料被改的人的所有好友;
* 前端 dispatcher 收到后调 loadFriendInfo 重拉资料
* <p>
* 此处 friendUserId 表示「资料被更新的那个人」(即对端),与 operatorUserId 一致
*/
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.system.api.message.user;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理员用户资料(昵称 / 头像)变更消息
* <p>
* 仅当 nickname 或 avatar 真的发生变化时才发送;订阅方据此做下游分发,
* 例如 IM 模块向该用户的所有好友推送 FRIEND_INFO_UPDATED 通知
*
* @author 芋道源码
*/
@Data
public class AdminUserProfileUpdateMessage {
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Long userId;
/**
* 变更后的昵称(无变更时为 null)
*/
private String nickname;
/**
* 变更后的头像(无变更时为 null)
*/
private String avatar;
}
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.system.mq.producer.user;
import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* 管理员用户 Producer
*
* @author 芋道源码
*/
@Slf4j
@Component
public class AdminUserProducer {
@Resource
private ApplicationContext applicationContext;
/**
* 发送 {@link AdminUserProfileUpdateMessage} 消息
*
* @param userId 用户编号
* @param nickname 变更后的昵称(无变更传 null)
* @param avatar 变更后的头像(无变更传 null)
*/
public void sendUserProfileUpdateMessage(Long userId, String nickname, String avatar) {
applicationContext.publishEvent(new AdminUserProfileUpdateMessage()
.setUserId(userId).setNickname(nickname).setAvatar(avatar));
}
}
@@ -25,6 +25,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper;
import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper;
import cn.iocoder.yudao.module.system.mq.producer.user.AdminUserProducer;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import cn.iocoder.yudao.module.system.service.dept.PostService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
@@ -88,6 +89,9 @@ public class AdminUserServiceImpl implements AdminUserService {
@Resource
private ConfigApi configApi;
@Resource
private AdminUserProducer adminUserProducer;
@Override
@Transactional(rollbackFor = Exception.class)
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE, bizNo = "{{#user.id}}",
@@ -158,6 +162,8 @@ public class AdminUserServiceImpl implements AdminUserService {
userMapper.updateById(updateObj);
// 2.2 更新岗位
updateUserPost(updateReqVO, updateObj);
// 2.3 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED)
publishUserProfileUpdatedIfChanged(oldUser, updateReqVO.getNickname(), updateReqVO.getAvatar());
// 3. 记录操作日志上下文
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldUser, UserSaveReqVO.class));
@@ -188,12 +194,30 @@ public class AdminUserServiceImpl implements AdminUserService {
@Override
public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) {
// 校验正确性
validateUserExists(id);
// 1. 校验正确性
AdminUserDO oldUser = validateUserExists(id);
validateEmailUnique(id, reqVO.getEmail());
validateMobileUnique(id, reqVO.getMobile());
// 执行更新
// 2. 执行更新
userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id));
// 3. 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED)
publishUserProfileUpdatedIfChanged(oldUser, reqVO.getNickname(), reqVO.getAvatar());
}
/**
* 仅当 nickname 或 avatar 跟旧值不一致时,发送 AdminUserProfileUpdateMessage
*/
private void publishUserProfileUpdatedIfChanged(AdminUserDO oldUser, String newNickname, String newAvatar) {
boolean nicknameChanged = newNickname != null && !ObjUtil.equal(oldUser.getNickname(), newNickname);
boolean avatarChanged = newAvatar != null && !ObjUtil.equal(oldUser.getAvatar(), newAvatar);
if (!nicknameChanged && !avatarChanged) {
return;
}
adminUserProducer.sendUserProfileUpdateMessage(oldUser.getId(),
nicknameChanged ? newNickname : null,
avatarChanged ? newAvatar : null);
}
@Override