Commit 479af56a08cbb982d2de38dc467f1cf0dc0ac53a

Authored by YunaiV
1 parent bfd374ee

✨ feat(im): 增加好友申请的逻辑(v1)

Showing 51 changed files with 1248 additions and 199 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
... ... @@ -57,17 +57,8 @@ public class ImFriendController {
57 57 return success(buildFriendRespVO(friend));
58 58 }
59 59  
60   - @PostMapping("/add")
61   - @Operation(summary = "添加好友(双向建立关系)")
62   - @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
63   - public CommonResult<Boolean> addFriend(
64   - @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
65   - friendService.addFriend(getLoginUserId(), friendUserId);
66   - return success(true);
67   - }
68   -
69 60 @DeleteMapping("/delete")
70   - @Operation(summary = "删除好友(双向软删除)")
  61 + @Operation(summary = "删除好友(单向软删除)")
71 62 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
72 63 public CommonResult<Boolean> deleteFriend(
73 64 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
... ... @@ -76,12 +67,30 @@ public class ImFriendController {
76 67 }
77 68  
78 69 @PutMapping("/update")
79   - @Operation(summary = "更新好友信息")
  70 + @Operation(summary = "更新好友单边属性(备注 / 免打扰 / 联系人置顶)")
80 71 public CommonResult<Boolean> updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
81 72 friendService.updateFriend(getLoginUserId(), reqVO);
82 73 return success(true);
83 74 }
84 75  
  76 + @PutMapping("/block")
  77 + @Operation(summary = "拉黑好友(必须先是好友;单边屏蔽对方私聊消息)")
  78 + @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
  79 + public CommonResult<Boolean> blockFriend(
  80 + @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
  81 + friendService.blockFriend(getLoginUserId(), friendUserId);
  82 + return success(true);
  83 + }
  84 +
  85 + @PutMapping("/unblock")
  86 + @Operation(summary = "移出黑名单")
  87 + @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
  88 + public CommonResult<Boolean> unblockFriend(
  89 + @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
  90 + friendService.unblockFriend(getLoginUserId(), friendUserId);
  91 + return success(true);
  92 + }
  93 +
85 94 // ========== 私有方法:VO 组装 ==========
86 95  
87 96 private List<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.friend;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestRespVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  10 +import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
  11 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  12 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.Parameter;
  15 +import io.swagger.v3.oas.annotations.tags.Tag;
  16 +import jakarta.annotation.Resource;
  17 +import jakarta.validation.Valid;
  18 +import jakarta.validation.constraints.NotNull;
  19 +import jakarta.validation.constraints.Size;
  20 +import org.springframework.validation.annotation.Validated;
  21 +import org.springframework.web.bind.annotation.*;
  22 +
  23 +import java.util.Collections;
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.Set;
  27 +import java.util.stream.Stream;
  28 +
  29 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  30 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  31 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
  32 +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
  33 +
  34 +/**
  35 + * IM 好友申请记录 Controller
  36 + *
  37 + * @author 芋道源码
  38 + */
  39 +@Tag(name = "管理后台 - IM 好友申请")
  40 +@RestController
  41 +@RequestMapping("/im/friend-request")
  42 +@Validated
  43 +public class ImFriendRequestController {
  44 +
  45 + @Resource
  46 + private ImFriendRequestService friendRequestService;
  47 +
  48 + @Resource
  49 + private AdminUserApi adminUserApi;
  50 +
  51 + @PostMapping("/apply")
  52 + @Operation(summary = "发起好友申请")
  53 + public CommonResult<Long> applyFriend(@Valid @RequestBody ImFriendRequestApplyReqVO reqVO) {
  54 + ImFriendRequestDO request = friendRequestService.applyFriend(getLoginUserId(), reqVO);
  55 + return success(request != null ? request.getId() : null);
  56 + }
  57 +
  58 + @PutMapping("/agree")
  59 + @Operation(summary = "同意好友申请")
  60 + @Parameter(name = "id", description = "申请编号", required = true, example = "1024")
  61 + public CommonResult<Boolean> agreeFriendRequest(
  62 + @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
  63 + friendRequestService.agreeFriendRequest(getLoginUserId(), id);
  64 + return success(true);
  65 + }
  66 +
  67 + @PutMapping("/refuse")
  68 + @Operation(summary = "拒绝好友申请")
  69 + public CommonResult<Boolean> refuseFriendRequest(
  70 + @RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
  71 + @RequestParam(value = "handleContent", required = false)
  72 + @Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
  73 + friendRequestService.refuseFriendRequest(getLoginUserId(), id, handleContent);
  74 + return success(true);
  75 + }
  76 +
  77 + @GetMapping("/list")
  78 + @Operation(summary = "查询「我相关」的好友申请列表(含我发起的、别人加我的)")
  79 + public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList() {
  80 + List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId());
  81 + return success(buildList(list));
  82 + }
  83 +
  84 + // ========== 私有方法:VO 组装 ==========
  85 +
  86 + private List<ImFriendRequestRespVO> buildList(List<ImFriendRequestDO> list) {
  87 + if (CollUtil.isEmpty(list)) {
  88 + return Collections.emptyList();
  89 + }
  90 + Set<Long> userIds = convertSetByFlatMap(list, r -> Stream.of(r.getFromUserId(), r.getToUserId()));
  91 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
  92 + return convertList(list, r -> {
  93 + ImFriendRequestRespVO vo = BeanUtils.toBean(r, ImFriendRequestRespVO.class);
  94 + MapUtils.findAndThen(userMap, r.getFromUserId(), user ->
  95 + vo.setFromNickname(user.getNickname()).setFromAvatar(user.getAvatar()));
  96 + MapUtils.findAndThen(userMap, r.getToUserId(), user ->
  97 + vo.setToNickname(user.getNickname()).setToAvatar(user.getAvatar()));
  98 + return vo;
  99 + });
  100 + }
  101 +
  102 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java
... ... @@ -29,6 +29,15 @@ public class ImFriendRespVO {
29 29 @Schema(description = "好友展示备注的拼音(小写无空格)", example = "laozhang")
30 30 private String displayNamePinyin;
31 31  
  32 + @Schema(description = "添加来源", example = "1")
  33 + private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
  34 +
  35 + @Schema(description = "是否置顶联系人", example = "false")
  36 + private Boolean pinned;
  37 +
  38 + @Schema(description = "是否拉黑(仅自己可见)", example = "false")
  39 + private Boolean blocked;
  40 +
32 41 @Schema(description = "好友状态", example = "0")
33 42 private Integer status;
34 43  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendUpdateReqVO.java
... ... @@ -20,4 +20,7 @@ public class ImFriendUpdateReqVO {
20 20 @Size(max = 16, message = "好友备注最多 16 个字符")
21 21 private String displayName;
22 22  
  23 + @Schema(description = "是否置顶联系人;不传表示不修改", example = "true")
  24 + private Boolean pinned;
  25 +
23 26 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestApplyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import jakarta.validation.constraints.Size;
  8 +import lombok.Data;
  9 +
  10 +/**
  11 + * IM 好友申请 - 发起 Request VO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Schema(description = "管理后台 - IM 好友申请发起 Request VO")
  16 +@Data
  17 +public class ImFriendRequestApplyReqVO {
  18 +
  19 + @Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  20 + @NotNull(message = "接收方用户编号不能为空")
  21 + private Long toUserId;
  22 +
  23 + @Schema(description = "申请理由", example = "我是芋艿(一种食材)")
  24 + @Size(max = 255, message = "申请理由最多 255 个字符")
  25 + private String applyContent;
  26 +
  27 + @Schema(description = "对接收方的备注(仅自己可见)", example = "老张")
  28 + @Size(max = 16, message = "好友备注最多 16 个字符")
  29 + private String displayName;
  30 +
  31 + @Schema(description = "添加来源", example = "1")
  32 + @InEnum(ImFriendAddSourceEnum.class)
  33 + private Integer addSource;
  34 +
  35 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +/**
  9 + * IM 好友申请 Response VO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Schema(description = "管理后台 - IM 好友申请 Response VO")
  14 +@Data
  15 +public class ImFriendRequestRespVO {
  16 +
  17 + @Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + private Long id;
  19 +
  20 + @Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
  21 + private Long fromUserId;
  22 +
  23 + @Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
  24 + private Long toUserId;
  25 +
  26 + @Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  27 + private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举
  28 +
  29 + @Schema(description = "申请理由", example = "我是芋艿(一种食材)")
  30 + private String applyContent;
  31 +
  32 + @Schema(description = "处理理由(接收方拒绝时可选填)", example = "暂不通过")
  33 + private String handleContent;
  34 +
  35 + @Schema(description = "添加来源", example = "1")
  36 + private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
  37 +
  38 + @Schema(description = "处理时间")
  39 + private LocalDateTime handleTime;
  40 +
  41 + @Schema(description = "申请创建时间")
  42 + private LocalDateTime createTime;
  43 +
  44 + // ========== 下面是聚合字段,方便前端显示 ==========
  45 +
  46 + @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道")
  47 + private String fromNickname;
  48 +
  49 + @Schema(description = "发起方头像(实时聚合自 AdminUser)")
  50 + private String fromAvatar;
  51 +
  52 + @Schema(description = "接收方昵称(实时聚合自 AdminUser)", example = "老张")
  53 + private String toNickname;
  54 +
  55 + @Schema(description = "接收方头像(实时聚合自 AdminUser)")
  56 + private String toAvatar;
  57 +
  58 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java
... ... @@ -16,6 +16,8 @@ import java.time.LocalDateTime;
16 16 * - 双向关系:A-B 互为好友会存 2 条记录(userId=A, friendUserId=B 和 userId=B, friendUserId=A)
17 17 * - 状态管理:{@link #status} 使用 {@link CommonStatusEnum},ENABLE=正常,DISABLE=已删除
18 18 * - 免打扰:{@link #muted} 控制是否屏蔽来自该好友的通知
  19 + * - 联系人置顶:{@link #pinned} 单边,影响联系人 / 会话排序
  20 + * - 黑名单:{@link #blocked} 弱关联 friend,单边屏蔽对方消息(必须先是好友)
19 21 *
20 22 * @author 芋道源码
21 23 */
... ... @@ -55,6 +57,20 @@ public class ImFriendDO extends BaseDO {
55 57 */
56 58 private String displayName;
57 59 /**
  60 + * 添加来源
  61 + * <p>
  62 + * 枚举 {@link cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum}
  63 + */
  64 + private Integer addSource;
  65 + /**
  66 + * 是否置顶联系人
  67 + */
  68 + private Boolean pinned;
  69 + /**
  70 + * 是否拉黑(弱关联 friend,单边屏蔽对方私聊消息)
  71 + */
  72 + private Boolean blocked;
  73 + /**
58 74 * 好友状态
59 75 * <p>
60 76 * 枚举 {@link CommonStatusEnum}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendRequestDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
  5 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableId;
  8 +import com.baomidou.mybatisplus.annotation.TableName;
  9 +import lombok.*;
  10 +
  11 +import java.time.LocalDateTime;
  12 +
  13 +/**
  14 + * IM 好友申请记录 DO
  15 + * <p>
  16 + * 配合「申请 - 审批」流程:
  17 + * - 发起方调 apply 接口落库(handleResult=UNHANDLED)
  18 + * - 接收方调 agree / refuse 处理(更新 handleResult / handleTime / handleContent)
  19 + * - 申请通过后,displayName / addSource 同步写入 {@link ImFriendDO}
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@TableName("im_friend_request")
  24 +@KeySequence("im_friend_request_seq")
  25 +@Data
  26 +@EqualsAndHashCode(callSuper = true)
  27 +@ToString(callSuper = true)
  28 +@Builder
  29 +@NoArgsConstructor
  30 +@AllArgsConstructor
  31 +public class ImFriendRequestDO extends BaseDO {
  32 +
  33 + /**
  34 + * 编号
  35 + */
  36 + @TableId
  37 + private Long id;
  38 + /**
  39 + * 发起方用户编号
  40 + * <p>
  41 + * 关联 AdminUserDO 的 id 字段
  42 + */
  43 + private Long fromUserId;
  44 + /**
  45 + * 接收方用户编号
  46 + * <p>
  47 + * 关联 AdminUserDO 的 id 字段
  48 + */
  49 + private Long toUserId;
  50 +
  51 + // ========== 申请发起阶段(发起方填写) ==========
  52 + /**
  53 + * 申请理由
  54 + */
  55 + private String applyContent;
  56 + /**
  57 + * 发起方对接收方的备注
  58 + * <p>
  59 + * 同意后写入 A 侧 {@link ImFriendDO#getDisplayName()};B 侧不动
  60 + */
  61 + private String displayName;
  62 + /**
  63 + * 添加来源
  64 + * <p>
  65 + * 枚举 {@link ImFriendAddSourceEnum}
  66 + */
  67 + private Integer addSource;
  68 +
  69 + // ========== 申请处理阶段(接收方填写) ==========
  70 + /**
  71 + * 处理结果
  72 + * <p>
  73 + * 枚举 {@link ImFriendRequestHandleResultEnum}
  74 + */
  75 + private Integer handleResult;
  76 + /**
  77 + * 处理理由(接收方拒绝时可选填)
  78 + */
  79 + private String handleContent;
  80 + /**
  81 + * 处理时间
  82 + */
  83 + private LocalDateTime handleTime;
  84 +
  85 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IM 好友申请记录 Mapper
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Mapper
  16 +public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> {
  17 +
  18 + default ImFriendRequestDO selectLatestByFromUserIdAndToUserId(Long fromUserId, Long toUserId) {
  19 + return selectOne(new LambdaQueryWrapperX<ImFriendRequestDO>()
  20 + .eq(ImFriendRequestDO::getFromUserId, fromUserId)
  21 + .eq(ImFriendRequestDO::getToUserId, toUserId)
  22 + .orderByDesc(ImFriendRequestDO::getId)
  23 + .last("LIMIT 1"));
  24 + }
  25 +
  26 + default List<ImFriendRequestDO> selectMyList(Long userId, int limit) {
  27 + // 先放扩展过滤再放双向 OR;否则 .and() 返回 LambdaQueryWrapper 基类,丢失 eqIfPresent 等扩展方法
  28 + LambdaQueryWrapperX<ImFriendRequestDO> wrapper = new LambdaQueryWrapperX<>();
  29 + wrapper.and(w -> w.eq(ImFriendRequestDO::getFromUserId, userId)
  30 + .or().eq(ImFriendRequestDO::getToUserId, userId))
  31 + .orderByDesc(ImFriendRequestDO::getId)
  32 + .last("LIMIT " + limit);
  33 + return selectList(wrapper);
  34 + }
  35 +
  36 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java
... ... @@ -16,12 +16,12 @@ public interface RedisKeyConstants {
16 16 String GROUP_MESSAGE_READ = "im:group:message:read:%s";
17 17  
18 18 /**
19   - * 好友关系是否存在的缓存
  19 + * 好友关系状态缓存(合并「是否好友」+「是否拉黑」两态)
20 20 * <p>
21   - * KEY 格式:friend:{userId}_{friendUserId}
22   - * VALUE 数据类型:Boolean
  21 + * KEY 格式:friend_state:{userId}_{friendUserId}
  22 + * VALUE 数据类型:{@link cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum}
23 23 */
24   - String FRIEND = "friend";
  24 + String FRIEND_STATE = "friend_state";
25 25  
26 26 /**
27 27 * 群信息缓存
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
... ... @@ -45,6 +45,14 @@ public interface ErrorCodeConstants {
45 45 // ========== 好友 (1-040-600-000) ==========
46 46 ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友");
47 47 ErrorCode FRIEND_ADD_SELF = new ErrorCode(1_040_600_002, "不允许添加自己为好友");
  48 + ErrorCode FRIEND_NOT_BLOCKED = new ErrorCode(1_040_600_003, "对方未在黑名单中");
  49 + ErrorCode FRIEND_BLOCKED_BY_PEER = new ErrorCode(1_040_600_004, "您已被对方拉入黑名单,无法发送消息");
  50 +
  51 + // ========== 好友申请 (1-040-610-000) ==========
  52 + ErrorCode FRIEND_REQUEST_NOT_EXISTS = new ErrorCode(1_040_610_001, "好友申请不存在");
  53 + ErrorCode FRIEND_REQUEST_HANDLED = new ErrorCode(1_040_610_002, "好友申请已处理");
  54 + ErrorCode FRIEND_REQUEST_NOT_TO_ME = new ErrorCode(1_040_610_003, "不能处理别人的好友申请");
  55 + ErrorCode FRIEND_REQUEST_TARGET_INVALID = new ErrorCode(1_040_610_004, "好友申请的对方不存在或已禁用");
48 56  
49 57 // ========== 敏感词 (1-040-700-000) ==========
50 58 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在");
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/friend/ImFriendAddSourceEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.enums.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * IM 好友添加来源枚举
  11 + * <p>
  12 + * 由发起方调用 apply 接口时传入;同意后同步写入 im_friend.add_source(双向)
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@RequiredArgsConstructor
  17 +@Getter
  18 +public enum ImFriendAddSourceEnum implements ArrayValuable<Integer> {
  19 +
  20 + // TODO DONE @AI:GROUP 提前到 source=2(先实现);BUSINESS_CARD 简化为 CARD
  21 + SEARCH(1, "搜索"), // FriendAddDialog 搜索流程
  22 + GROUP(2, "群聊"), // TODO @AI:群成员主页加好友,待落 UI 入口
  23 + QR_CODE(3, "扫码"), // TODO @芋艿:后续实现扫码加好友
  24 + CARD(4, "名片"); // TODO @芋艿:后续实现通过名片加好友,类似微信的「扫一扫 - 名片」功能,或者「通讯录 - 推荐好友」功能
  25 +
  26 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImFriendAddSourceEnum::getSource).toArray(Integer[]::new);
  27 +
  28 + /**
  29 + * 来源
  30 + */
  31 + private final Integer source;
  32 + /**
  33 + * 名字
  34 + */
  35 + private final String name;
  36 +
  37 + @Override
  38 + public Integer[] array() {
  39 + return ARRAYS;
  40 + }
  41 +
  42 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/friend/ImFriendRequestHandleResultEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.enums.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +import java.util.Objects;
  9 +
  10 +/**
  11 + * IM 好友申请处理结果枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@RequiredArgsConstructor
  16 +@Getter
  17 +public enum ImFriendRequestHandleResultEnum implements ArrayValuable<Integer> {
  18 +
  19 + UNHANDLED(0, "未处理"),
  20 + AGREED(1, "同意"),
  21 + REFUSED(2, "拒绝");
  22 +
  23 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImFriendRequestHandleResultEnum::getResult).toArray(Integer[]::new);
  24 +
  25 + /**
  26 + * 结果
  27 + */
  28 + private final Integer result;
  29 + /**
  30 + * 名字
  31 + */
  32 + private final String name;
  33 +
  34 + @Override
  35 + public Integer[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 + /**
  40 + * 判断申请是否还未处理
  41 + */
  42 + public static boolean isUnhandled(Integer result) {
  43 + return Objects.equals(UNHANDLED.result, result);
  44 + }
  45 +
  46 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/friend/ImFriendStateEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.enums.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * IM 好友关系状态(合并「是否好友」+「是否拉黑」两态)
  11 + * <p>
  12 + * 用 state 而非 status:避免和 ImFriendDO 的 statue 物理字段(CommonStatusEnum:ENABLE / DISABLE)混淆。
  13 + * <p>
  14 + * 用于 ImFriendService.getFriendState 返回值与 FRIEND_STATE 缓存值:私聊发送热点路径下,sender 调缓存即可同时判定「能否发」和「是否屏蔽接收方」
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@RequiredArgsConstructor
  19 +@Getter
  20 +public enum ImFriendStateEnum implements ArrayValuable<Integer> {
  21 +
  22 + NONE(0, "非好友 / 已删除"),
  23 + FRIEND(1, "好友"),
  24 + BLOCKED(2, "好友且已被对方拉黑");
  25 +
  26 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImFriendStateEnum::getState).toArray(Integer[]::new);
  27 +
  28 + /**
  29 + * 状态值
  30 + */
  31 + private final Integer state;
  32 + /**
  33 + * 名字
  34 + */
  35 + private final String name;
  36 +
  37 + @Override
  38 + public Integer[] array() {
  39 + return ARRAYS;
  40 + }
  41 +
  42 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
... ... @@ -33,10 +33,63 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
33 33 TIP_TIME(20, "时间分隔", false, false), // 暂无(前端本地生成)
34 34 TIP_TEXT(21, "系统提示", true, false), // 对应 TextMessage 类
35 35  
36   - // ========== 好友变更 ==========
37   - FRIEND_ADD(100, "好友添加", false, false), // 暂无对应类
38   - FRIEND_DELETE(101, "好友删除", false, false), // 暂无对应类
39   - FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取)
  36 + // ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
  37 + /**
  38 + * 对应 OpenIM:FriendApplicationApprovedNotification 1201
  39 + * 对应自己的类:FriendRequestApprovedNotification
  40 + * 场景:B 同意 A 的好友申请,推给 A 多端
  41 + */
  42 + FRIEND_REQUEST_APPROVED(1201, "好友申请被同意", false, false),
  43 + /**
  44 + * 对应 OpenIM:FriendApplicationRejectedNotification 1202
  45 + * 对应自己的类:FriendRequestRejectedNotification
  46 + * 场景:B 拒绝 A 的好友申请,推给 A 多端
  47 + */
  48 + FRIEND_REQUEST_REJECTED(1202, "好友申请被拒绝", false, false),
  49 + /**
  50 + * 对应 OpenIM:FriendApplicationNotification 1203
  51 + * 对应自己的类:FriendRequestNotification
  52 + * 场景:A 申请加 B,推给 B 多端,前端落到「新的朋友」列表
  53 + */
  54 + FRIEND_APPLICATION(1203, "收到新的好友申请", false, false),
  55 + /**
  56 + * 对应 OpenIM:FriendAddedNotification 1204
  57 + * 对应自己的类:FriendAddNotification
  58 + * 场景:双方建立好友关系(同意申请 / 管理员导入),推给 A、B 双方多端
  59 + */
  60 + FRIEND_ADD(1204, "新增好友", false, false),
  61 + /**
  62 + * 对应 OpenIM:FriendDeletedNotification 1205
  63 + * 对应自己的类:FriendDeleteNotification
  64 + * 场景:A 删除 B,推给 A、B 双方多端
  65 + */
  66 + FRIEND_DELETE(1205, "好友被删除", false, false),
  67 + // 1206 对应 OpenIM FriendRemarkSetNotification;本系统并入 FRIEND_UPDATE(1210) 统一推送,单一字段变更不再独立通道
  68 + /**
  69 + * 对应 OpenIM:BlackAddedNotification 1207
  70 + * 对应自己的类:FriendBlockNotification
  71 + * 场景:A 拉黑 B,仅推 A 多端
  72 + */
  73 + FRIEND_BLOCK(1207, "加入黑名单", false, false),
  74 + /**
  75 + * 对应 OpenIM:BlackDeletedNotification 1208
  76 + * 对应自己的类:FriendUnblockNotification
  77 + * 场景:A 移出 B 的黑名单,仅推 A 多端
  78 + */
  79 + FRIEND_UNBLOCK(1208, "移出黑名单", false, false),
  80 + /**
  81 + * 对应 OpenIM:FriendInfoUpdatedNotification 1209
  82 + * 对应自己的类:FriendInfoUpdatedNotification
  83 + * 场景:B 改了昵称 / 头像后,推给 B 的所有好友
  84 + * TODO @AI:未实现;待 system 模块改昵称 / 头像时回调 IM 模块批量推此事件,前端 dispatcher 已就绪
  85 + */
  86 + FRIEND_INFO_UPDATED(1209, "好友资料变更", false, false),
  87 + /**
  88 + * 对应 OpenIM:FriendsInfoUpdateNotification 1210(窄化到 muted / pinned 单边属性)
  89 + * 对应自己的类:FriendUpdateNotification
  90 + * 场景:A 改了 muted / pinned 等单边属性,推 A 多端
  91 + */
  92 + FRIEND_UPDATE(1210, "好友信息批量更新", false, false),
40 93  
41 94 // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ==========
42 95 // 1500 对应 OpenIM GroupNotificationBegin 起始位,仅作占位,不使用
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.friend;
  2 +
  3 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * IM 好友申请 Service 接口
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +public interface ImFriendRequestService {
  14 +
  15 + /**
  16 + * 发起好友申请
  17 + *
  18 + * @param fromUserId 发起方用户编号
  19 + * @param reqVO 申请请求
  20 + * @return 申请记录
  21 + */
  22 + ImFriendRequestDO applyFriend(Long fromUserId, ImFriendRequestApplyReqVO reqVO);
  23 +
  24 + /**
  25 + * 同意好友申请
  26 + *
  27 + * @param userId 操作人用户编号
  28 + * @param requestId 申请记录编号
  29 + */
  30 + void agreeFriendRequest(Long userId, Long requestId);
  31 +
  32 + /**
  33 + * 拒绝好友申请
  34 + *
  35 + * @param userId 操作人用户编号
  36 + * @param requestId 申请记录编号
  37 + * @param handleContent 拒绝理由
  38 + */
  39 + void refuseFriendRequest(Long userId, Long requestId, String handleContent);
  40 +
  41 + /**
  42 + * 查询「我相关」的申请列表(含我发起的、别人加我的)
  43 + *
  44 + * @param userId 用户编号
  45 + * @return 申请记录列表
  46 + */
  47 + List<ImFriendRequestDO> getMyFriendRequestList(Long userId);
  48 +
  49 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.friend;
  2 +
  3 +import cn.hutool.core.util.BooleanUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
  10 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  11 +import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
  12 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
  13 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  14 +import cn.iocoder.yudao.module.im.framework.config.ImProperties;
  15 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  16 +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  17 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestApprovedNotification;
  18 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestNotification;
  19 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestRejectedNotification;
  20 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  21 +import jakarta.annotation.Resource;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.springframework.stereotype.Service;
  24 +import org.springframework.transaction.annotation.Transactional;
  25 +import org.springframework.transaction.support.TransactionSynchronization;
  26 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  27 +import org.springframework.validation.annotation.Validated;
  28 +
  29 +import java.time.LocalDateTime;
  30 +import java.util.List;
  31 +import java.util.Objects;
  32 +
  33 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  34 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
  35 +
  36 +/**
  37 + * IM 好友申请 Service 实现类
  38 + *
  39 + * @author 芋道源码
  40 + */
  41 +@Slf4j
  42 +@Service
  43 +@Validated
  44 +public class ImFriendRequestServiceImpl implements ImFriendRequestService {
  45 +
  46 + @Resource
  47 + private ImFriendRequestMapper friendRequestMapper;
  48 +
  49 + @Resource
  50 + private ImFriendService friendService;
  51 + @Resource
  52 + private ImWebSocketService websocketService;
  53 +
  54 + @Resource
  55 + private ImProperties imProperties;
  56 +
  57 + @Resource
  58 + private AdminUserApi adminUserApi;
  59 +
  60 + @Override
  61 + @Transactional(rollbackFor = Exception.class)
  62 + public ImFriendRequestDO applyFriend(Long fromUserId, ImFriendRequestApplyReqVO reqVO) {
  63 + Long toUserId = reqVO.getToUserId();
  64 + // 1.1 校验:不能加自己
  65 + if (Objects.equals(fromUserId, toUserId)) {
  66 + throw exception(FRIEND_ADD_SELF);
  67 + }
  68 + // 1.2 校验对方存在且启用
  69 + adminUserApi.validateUser(toUserId);
  70 + // 1.3 已是好友:直接幂等返回
  71 + ImFriendDO friend = friendService.getFriend(fromUserId, toUserId);
  72 + if (friend != null && CommonStatusEnum.isEnable(friend.getStatus())) {
  73 + return null;
  74 + }
  75 + // 1.4 被对方拉黑:静默成功,不暴露
  76 + // TODO @AI:是不是 peerFriend?这样更好理解。
  77 + // TODO @AI:是不是应该提示被拉黑了?返回 null 给前端很奇怪。
  78 + ImFriendDO reverse = friendService.getFriend(toUserId, fromUserId);
  79 + if (reverse != null && BooleanUtil.isTrue(reverse.getBlocked())) {
  80 + log.info("[applyFriend][fromUserId({}) 被 toUserId({}) 拉黑,静默忽略]", fromUserId, toUserId);
  81 + return null;
  82 + }
  83 +
  84 + // 2. 落库:复用最新一条未处理记录;否则新建
  85 + ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId);
  86 + if (request != null && ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) {
  87 + // 复用未处理记录:刷新申请理由 / 备注 / 来源;createTime 维持首次申请时间不变(与列表 id DESC 排序一致)
  88 + BeanUtils.copyProperties(reqVO, request);
  89 + friendRequestMapper.updateById(request);
  90 + } else {
  91 + request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
  92 + .setFromUserId(fromUserId).setToUserId(toUserId)
  93 + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult());
  94 + friendRequestMapper.insert(request);
  95 + }
  96 +
  97 + // 3. 推送 FRIEND_APPLICATION 给 toUser 多端
  98 + FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification()
  99 + .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource())
  100 + .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
  101 + websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
  102 + ImMessageTypeEnum.FRIEND_APPLICATION.getType(), fromUserId, toUserId, payload));
  103 +
  104 + // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程
  105 + if (imProperties.getFriend().isAutoAccept()) {
  106 + Long requestId = request.getId();
  107 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  108 +
  109 + @Override
  110 + public void afterCommit() {
  111 + getSelf().agreeFriendRequest(toUserId, requestId);
  112 + }
  113 +
  114 + });
  115 + }
  116 + return request;
  117 + }
  118 +
  119 + @Override
  120 + @Transactional(rollbackFor = Exception.class)
  121 + public void agreeFriendRequest(Long userId, Long requestId) {
  122 + // 1. 校验申请存在 + 未处理 + 操作人是接收方
  123 + ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
  124 +
  125 + // 2. 更新申请:handleResult=AGREED + handleTime
  126 + LocalDateTime now = LocalDateTime.now();
  127 + friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId())
  128 + .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now));
  129 + request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now);
  130 +
  131 + // 3. 双向建立好友关系
  132 + friendService.becomeFriends(request);
  133 +
  134 + // 4. 推 FRIEND_REQUEST_APPROVED 给 fromUser 多端
  135 + FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification)
  136 + new FriendRequestApprovedNotification().setRequestId(request.getId())
  137 + .setOperatorUserId(userId).setFriendUserId(userId);
  138 + websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(
  139 + ImMessageTypeEnum.FRIEND_REQUEST_APPROVED.getType(), userId, request.getFromUserId(), payload));
  140 + }
  141 +
  142 + @Override
  143 + @Transactional(rollbackFor = Exception.class)
  144 + public void refuseFriendRequest(Long userId, Long requestId, String handleContent) {
  145 + // 1. 校验申请存在 + 未处理 + 操作人是接收方
  146 + ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
  147 +
  148 + // 2. 更新申请:handleResult=REFUSED + handleContent + handleTime
  149 + LocalDateTime now = LocalDateTime.now();
  150 + friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId())
  151 + .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult())
  152 + .setHandleContent(handleContent).setHandleTime(now));
  153 +
  154 + // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端
  155 + FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification)
  156 + new FriendRequestRejectedNotification().setRequestId(request.getId())
  157 + .setHandleContent(handleContent)
  158 + .setOperatorUserId(userId).setFriendUserId(userId);
  159 + websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(
  160 + ImMessageTypeEnum.FRIEND_REQUEST_REJECTED.getType(), userId, request.getFromUserId(), payload));
  161 + }
  162 +
  163 + @Override
  164 + public List<ImFriendRequestDO> getMyFriendRequestList(Long userId) {
  165 + return friendRequestMapper.selectMyList(userId, imProperties.getFriend().getRequestListMaxSize());
  166 + }
  167 +
  168 + /**
  169 + * 校验申请可被「当前用户」处理:申请存在 + 未处理 + 操作人 = 接收方
  170 + */
  171 + private ImFriendRequestDO validateRequestForHandle(Long userId, Long requestId) {
  172 + ImFriendRequestDO request = friendRequestMapper.selectById(requestId);
  173 + if (request == null) {
  174 + throw exception(FRIEND_REQUEST_NOT_EXISTS);
  175 + }
  176 + if (ObjUtil.notEqual(request.getToUserId(), userId)) {
  177 + throw exception(FRIEND_REQUEST_NOT_TO_ME);
  178 + }
  179 + if (!ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) {
  180 + throw exception(FRIEND_REQUEST_HANDLED);
  181 + }
  182 + return request;
  183 + }
  184 +
  185 + private ImFriendRequestServiceImpl getSelf() {
  186 + return SpringUtil.getBean(getClass());
  187 + }
  188 +
  189 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
... ... @@ -4,88 +4,81 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
5 5 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
6 6 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
  7 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  8 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
7 9  
8 10 import java.util.Collection;
9 11 import java.util.List;
10 12  
11 13 /**
12 14 * IM 好友关系 Service 接口
  15 + * <p>
  16 + * 注意:用户端「加好友」走 {@link ImFriendRequestService#applyFriend} 申请-审批流程,
  17 + * 不再开放直接 add 接口;只有 {@link #becomeFriends} 是内部入口(被 agree 同意 / 管理员 import 触发)。
13 18 *
14 19 * @author 芋道源码
15 20 */
16 21 public interface ImFriendService {
17 22  
18 23 /**
19   - * 判断两个用户是否是好友关系(未删除状态)
20   - *
21   - * @param userId 用户编号
22   - * @param friendUserId 好友用户编号
23   - * @return 是否好友
  24 + * 获取 userId 视角下与 friendUserId 的好友关系状态(私聊发送热点路径)
  25 + * <p>
  26 + * 合并「是否好友」+「是否被自己拉黑」两态,供调用方一次缓存查询完成判定,详见 {@link ImFriendStateEnum}
24 27 */
25   - boolean isFriend(Long userId, Long friendUserId);
  28 + ImFriendStateEnum getFriendState(Long userId, Long friendUserId);
26 29  
27 30 /**
28 31 * 获得当前用户的好友列表(含已删除状态)
29   - * <p>
30   - * 返回的列表里**包含已删除状态**,前端按 status 区分展示(已删除的置灰);
31   - * 原因是本地已经建立会话的好友被单向删除后,前端仍需要展示头像/昵称。
32   - *
33   - * @param userId 用户编号
34   - * @return 好友关系列表(含已删除的记录)
35 32 */
36 33 List<ImFriendDO> getFriendList(Long userId);
37 34  
38 35 /**
39 36 * 获得当前用户与指定用户之间的有效好友列表(仅 ENABLE 状态)
40   - *
41   - * @param userId 用户编号
42   - * @param friendUserIds 需要校验的好友用户编号集合
43   - * @return 有效好友关系列表
44 37 */
45 38 List<ImFriendDO> getActiveFriendList(Long userId, Collection<Long> friendUserIds);
46 39  
47 40 /**
48   - * 查询一个好友关系记录(仅限本人视角)
49   - *
50   - * @param userId 当前登录用户编号
51   - * @param friendUserId 目标用户编号
52   - * @return 好友关系记录,不存在返回 null
  41 + * 查询一个好友关系记录
53 42 */
54 43 ImFriendDO getFriend(Long userId, Long friendUserId);
55 44  
  45 + // ==================== 内部入口 ====================
  46 +
56 47 /**
57   - * 添加好友(双向绑定)
  48 + * 双向建立好友关系(内部入口)
58 49 * <p>
59   - * 核心流程:
60   - * 1. 校验:不允许添加自己;对方必须存在
61   - * 2. 双向插入/更新关系(若已存在则刷新为未删除)
62   - * 3. 推送"你们已成为好友"系统提示(TODO)
  50 + * 由 {@link ImFriendRequestService#agreeFriendRequest} 同意申请 / 管理后台导入触发;
  51 + * A 侧 displayName / addSource 取自申请记录;B 侧 displayName 为空、addSource 同来源。
  52 + * 写库后推送 FRIEND_ADD 通知给 A、B 双方多端,并下发 TIP 系统消息。
63 53 *
64   - * @param userId 当前登录用户编号
65   - * @param friendUserId 好友的用户编号
  54 + * @param request 已同意的申请记录(决定 fromUserId / toUserId / addSource / displayName)
66 55 */
67   - void addFriend(Long userId, Long friendUserId);
  56 + void becomeFriends(ImFriendRequestDO request);
  57 +
  58 + // ==================== 用户端 ====================
68 59  
69 60 /**
70   - * 删除好友(双向软删除)
  61 + * 删除好友(单向软删除)
71 62 * <p>
72   - * 两边关系都标记为已删除状态。
73   - *
74   - * @param userId 当前登录用户编号
75   - * @param friendUserId 好友的用户编号
  63 + * 仅删除 userId 视角下的好友关系;对端 friendUserId 的视角不受影响(与 OpenIM 单边删除语义对齐)
76 64 */
77 65 void deleteFriend(Long userId, Long friendUserId);
78 66  
79 67 /**
80   - * 更新好友信息(仅更新本人→对方的单边关系属性)
81   - * <p>
82   - * 目前支持:免打扰(muted);后续可扩展备注等字段。
83   - *
84   - * @param userId 当前登录用户编号
85   - * @param reqVO 更新请求
  68 + * 更新好友单边属性(备注 / 免打扰 / 联系人置顶)
86 69 */
87 70 void updateFriend(Long userId, ImFriendUpdateReqVO reqVO);
88 71  
  72 + /**
  73 + * 拉黑好友(必须先是好友)
  74 + */
  75 + void blockFriend(Long userId, Long friendUserId);
  76 +
  77 + /**
  78 + * 移出黑名单
  79 + */
  80 + void unblockFriend(Long userId, Long friendUserId);
  81 +
89 82 // ==================== 管理后台 ====================
90 83  
91 84 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
  3 +import cn.hutool.core.util.BooleanUtil;
3 4 import cn.hutool.extra.spring.SpringUtil;
4 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
7 8 import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
8 9 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
  10 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
9 11 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
  12 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
  13 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
10 14 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
11 15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
12 16 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
13   -import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  17 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.*;
14 18 import jakarta.annotation.Resource;
15 19 import lombok.extern.slf4j.Slf4j;
16 20 import org.springframework.cache.annotation.CacheEvict;
17 21 import org.springframework.cache.annotation.Cacheable;
  22 +import org.springframework.cache.annotation.Caching;
18 23 import org.springframework.context.annotation.Lazy;
19   -import org.springframework.dao.DuplicateKeyException;
20 24 import org.springframework.stereotype.Service;
21 25 import org.springframework.transaction.annotation.Transactional;
22 26 import org.springframework.validation.annotation.Validated;
... ... @@ -24,11 +28,10 @@ import org.springframework.validation.annotation.Validated;
24 28 import java.time.LocalDateTime;
25 29 import java.util.Collection;
26 30 import java.util.List;
27   -import java.util.Objects;
28 31  
29 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
30   -import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND;
31   -import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
  33 +import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND_STATE;
  34 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_BLOCKED;
32 35 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
33 36 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_ADD_TIP_MESSAGE;
34 37 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_TIP_MESSAGE;
... ... @@ -44,138 +47,218 @@ import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.FRIEND_DELETE_T
44 47 public class ImFriendServiceImpl implements ImFriendService {
45 48  
46 49 @Resource
47   - private ImFriendMapper imFriendMapper;
  50 + private ImFriendMapper friendMapper;
48 51  
49 52 @Resource
50   - private AdminUserApi adminUserApi;
51   -
52   - @Resource
53   - private ImWebSocketService imWebSocketService;
  53 + private ImWebSocketService websocketService;
54 54 @Resource
55 55 @Lazy
56 56 private ImPrivateMessageService privateMessageService;
57 57  
58 58 @Override
59   - @Cacheable(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId", unless = "#result == null")
60   - public boolean isFriend(Long userId, Long friendUserId) {
61   - ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
62   - return friend != null && CommonStatusEnum.isEnable(friend.getStatus());
  59 + @Cacheable(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId", unless = "#result == null")
  60 + public ImFriendStateEnum getFriendState(Long userId, Long friendUserId) {
  61 + // 1.1 我侧记录:我方删了,都算非好友
  62 + ImFriendDO mine = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  63 + if (mine == null || !CommonStatusEnum.isEnable(mine.getStatus())) {
  64 + return ImFriendStateEnum.NONE;
  65 + }
  66 + // 1.2 对方侧记录:对方删了 = 不是好友
  67 + ImFriendDO peer = friendMapper.selectByUserIdAndFriendUserId(friendUserId, userId);
  68 + if (peer == null || !CommonStatusEnum.isEnable(peer.getStatus())) {
  69 + return ImFriendStateEnum.NONE;
  70 + }
  71 + // 2. 仅当双方都是 ENABLE 状态,才算好友关系;此时对方拉黑我,则是 BLOCKED
  72 + return BooleanUtil.isTrue(peer.getBlocked()) ? ImFriendStateEnum.BLOCKED : ImFriendStateEnum.FRIEND;
63 73 }
64 74  
65 75 @Override
66 76 public List<ImFriendDO> getFriendList(Long userId) {
67   - return imFriendMapper.selectListByUserId(userId);
  77 + return friendMapper.selectListByUserId(userId);
68 78 }
69 79  
70 80 @Override
71 81 public List<ImFriendDO> getActiveFriendList(Long userId, Collection<Long> friendUserIds) {
72   - return imFriendMapper.selectListByUserIdAndFriendUserIdsAndStatus(userId, friendUserIds,
  82 + return friendMapper.selectListByUserIdAndFriendUserIdsAndStatus(userId, friendUserIds,
73 83 CommonStatusEnum.ENABLE.getStatus());
74 84 }
75 85  
76 86 @Override
77 87 public ImFriendDO getFriend(Long userId, Long friendUserId) {
78   - return imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  88 + return friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
79 89 }
80 90  
81 91 @Override
82 92 public void updateFriend(Long userId, ImFriendUpdateReqVO reqVO) {
83   - // 1. 校验好友关系存在
84   - ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId());
  93 + // 1.1 校验:至少改一个字段(无字段变更,直接结束)
  94 + if (reqVO.getDisplayName() == null && reqVO.getMuted() == null && reqVO.getPinned() == null) {
  95 + return;
  96 + }
  97 + // 1.2 校验好友关系存在
  98 + ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId());
85 99 if (friend == null) {
86 100 throw exception(FRIEND_NOT_FRIEND);
87 101 }
88 102  
89   - // 2. 更新好友属性
90   - imFriendMapper.updateById(new ImFriendDO().setId(friend.getId())
91   - .setMuted(reqVO.getMuted()).setDisplayName(reqVO.getDisplayName()));
  103 + // 2. 更新好友属性(备注 / 免打扰 / 联系人置顶)
  104 + friendMapper.updateById(new ImFriendDO().setId(friend.getId())
  105 + .setMuted(reqVO.getMuted())
  106 + .setDisplayName(reqVO.getDisplayName())
  107 + .setPinned(reqVO.getPinned()));
92 108  
93   - // 3. 推送好友更新通知(多端同步)
94   - imWebSocketService.sendPrivateMessageAsync(userId,
95   - ImPrivateMessageDTO.ofFriendUpdate(userId, reqVO.getFriendUserId()));
  109 + // 3. 推 FRIEND_UPDATE 给 A 多端:所有单边属性变更合并为单条通知,避免多通知顺序竞争
  110 + FriendUpdateNotification payload = (FriendUpdateNotification) new FriendUpdateNotification()
  111 + .setDisplayName(reqVO.getDisplayName()).setMuted(reqVO.getMuted()).setPinned(reqVO.getPinned())
  112 + .setOperatorUserId(userId).setFriendUserId(reqVO.getFriendUserId());
  113 + websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
  114 + ImMessageTypeEnum.FRIEND_UPDATE.getType(), userId, userId, payload));
96 115 }
97 116  
98 117 @Override
99 118 @Transactional(rollbackFor = Exception.class)
100   - public void addFriend(Long userId, Long friendUserId) {
101   - // 1.1 校验:不允许添加自己为好友
102   - if (Objects.equals(userId, friendUserId)) {
103   - throw exception(FRIEND_ADD_SELF);
104   - }
105   - // 1.2 校验对方存在(禁用/不存在的账号不允许添加)
106   - adminUserApi.validateUser(friendUserId);
107   -
108   - // 2. 双向绑定
109   - getSelf().addFriend0(userId, friendUserId);
110   - getSelf().addFriend0(friendUserId, userId);
111   -
112   - // 3.1 插入 TIP_TEXT 系统提示消息并推送双方
113   - privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_ADD_TIP_MESSAGE);
114   - // 3.2 推送好友添加通知给双方
115   - imWebSocketService.sendPrivateMessageAsync(userId,
116   - ImPrivateMessageDTO.ofFriendAdd(friendUserId, userId));
117   - imWebSocketService.sendPrivateMessageAsync(friendUserId,
118   - ImPrivateMessageDTO.ofFriendAdd(userId, friendUserId));
  119 + public void becomeFriends(ImFriendRequestDO request) {
  120 + Long fromUserId = request.getFromUserId();
  121 + Long toUserId = request.getToUserId();
  122 + // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源
  123 + getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
  124 + getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource());
  125 +
  126 + // TODO DONE @AI:删除「在 openim 里」措辞;TIP 与 FRIEND_ADD 目的不同保持双发
  127 + // TODO @AI:在确认一次;是否需要发送 tip?而是通过 FRIEND_ADD 可以满足了。。。(讨论一下)
  128 + // 2.1 推 TIP 系统消息(双方私聊会话里看到「你们已成为好友」);TIP 走会话入库,FRIEND_ADD 走事件通知
  129 + privateMessageService.sendTipPrivateMessage(fromUserId, toUserId, FRIEND_ADD_TIP_MESSAGE);
  130 +
  131 + // 2.2.1 推 FRIEND_ADD 给 fromUser 多端
  132 + FriendAddNotification toFrom = (FriendAddNotification) new FriendAddNotification()
  133 + .setOperatorUserId(toUserId).setFriendUserId(toUserId);
  134 + websocketService.sendPrivateMessageAsync(fromUserId, ImPrivateMessageDTO.ofFriendNotification(
  135 + ImMessageTypeEnum.FRIEND_ADD.getType(), toUserId, fromUserId, toFrom));
  136 + // 2.2.2 推 FRIEND_ADD 给 toUser 多端
  137 + FriendAddNotification toTo = (FriendAddNotification) new FriendAddNotification()
  138 + .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
  139 + websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
  140 + ImMessageTypeEnum.FRIEND_ADD.getType(), fromUserId, toUserId, toTo));
119 141 }
120 142  
121 143 @Override
122 144 @Transactional(rollbackFor = Exception.class)
123 145 public void deleteFriend(Long userId, Long friendUserId) {
124   - // 1. 双向标记为 DISABLE + 记录 deleteTime
  146 + // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
125 147 getSelf().deleteFriend0(userId, friendUserId);
126   - getSelf().deleteFriend0(friendUserId, userId);
127 148  
128   - // 2.1 插入 TIP_TEXT 系统提示消息并推送双方
  149 + // 2.1 推 TIP 系统消息
129 150 privateMessageService.sendTipPrivateMessage(userId, friendUserId, FRIEND_DELETE_TIP_MESSAGE);
130   - // 2.2 推送好友删除通知给双方
131   - imWebSocketService.sendPrivateMessageAsync(userId,
132   - ImPrivateMessageDTO.ofFriendDelete(userId, friendUserId));
133   - imWebSocketService.sendPrivateMessageAsync(friendUserId,
134   - ImPrivateMessageDTO.ofFriendDelete(friendUserId, userId));
  151 +
  152 + // 2.2 推 FRIEND_DELETE 给 userId 多端做同步(friendUserId 不感知)
  153 + FriendDeleteNotification payload = (FriendDeleteNotification) new FriendDeleteNotification()
  154 + .setOperatorUserId(userId).setFriendUserId(friendUserId);
  155 + websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
  156 + ImMessageTypeEnum.FRIEND_DELETE.getType(), userId, userId, payload));
  157 + }
  158 +
  159 + @Override
  160 + @Caching(evict = {
  161 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  162 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  163 + })
  164 + public void blockFriend(Long userId, Long friendUserId) {
  165 + // 1.1 校验是好友
  166 + ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  167 + if (friend == null || !CommonStatusEnum.isEnable(friend.getStatus())) {
  168 + throw exception(FRIEND_NOT_FRIEND);
  169 + }
  170 + // 1.2 已拉黑直接返回,幂等
  171 + if (BooleanUtil.isTrue(friend.getBlocked())) {
  172 + return;
  173 + }
  174 +
  175 + // 2. 单边更新
  176 + friendMapper.updateById(new ImFriendDO().setId(friend.getId()).setBlocked(true));
  177 +
  178 + // 3. 推 FRIEND_BLOCK 给 A 多端
  179 + FriendBlockNotification payload = (FriendBlockNotification) new FriendBlockNotification()
  180 + .setOperatorUserId(userId).setFriendUserId(friendUserId);
  181 + websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
  182 + ImMessageTypeEnum.FRIEND_BLOCK.getType(), userId, userId, payload));
  183 + }
  184 +
  185 + @Override
  186 + @Caching(evict = {
  187 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  188 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  189 + })
  190 + public void unblockFriend(Long userId, Long friendUserId) {
  191 + // 1.1 校验是好友
  192 + ImFriendDO friend = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  193 + if (friend == null || !CommonStatusEnum.isEnable(friend.getStatus())) {
  194 + throw exception(FRIEND_NOT_FRIEND);
  195 + }
  196 + // 1.2 未拉黑则报错
  197 + if (!BooleanUtil.isTrue(friend.getBlocked())) {
  198 + throw exception(FRIEND_NOT_BLOCKED);
  199 + }
  200 +
  201 + // 2. 单边更新
  202 + friendMapper.updateById(new ImFriendDO().setId(friend.getId()).setBlocked(false));
  203 +
  204 + // 3. 推 FRIEND_UNBLOCK 给 A 多端
  205 + FriendUnblockNotification payload = (FriendUnblockNotification) new FriendUnblockNotification()
  206 + .setOperatorUserId(userId).setFriendUserId(friendUserId);
  207 + websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
  208 + ImMessageTypeEnum.FRIEND_UNBLOCK.getType(), userId, userId, payload));
135 209 }
136 210  
137 211 /**
138   - * 单向绑定好友关系:
139   - * - 首次:新增记录(status=ENABLE,addTime=now)
140   - * - 已存在且 ENABLE:无需重复操作
141   - * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime)
  212 + * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} 调用):
  213 + * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置)
  214 + * - 情况二:不存在记录 → 直接插入新记录
142 215 * <p>
143   - * 并发安全:依靠 im_friend 表的唯一索引 uk_im_friend_user_friend(user_id, friend_user_id) 保证幂等,
144   - * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。
  216 + * 并发安全:业务侧通过 friend_request.handle_result 已保证 agree 不会并发触发;
  217 + * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。
145 218 */
146   - @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId")
147   - public void addFriend0(Long userId, Long friendUserId) {
148   - ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
149   - // 情况一:已存在记录 → 恢复或跳过
  219 + @Caching(evict = {
  220 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  221 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  222 + })
  223 + public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) {
  224 + ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  225 + // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐
150 226 if (exists != null) {
151   - if (CommonStatusEnum.isEnable(exists.getStatus())) {
152   - return;
  227 + ImFriendDO update = new ImFriendDO().setId(exists.getId())
  228 + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now())
  229 + .setMuted(false).setPinned(false).setBlocked(false);
  230 + if (displayName != null) {
  231 + update.setDisplayName(displayName);
153 232 }
154   - imFriendMapper.updateById(new ImFriendDO().setId(exists.getId())
155   - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now()));
  233 + if (addSource != null) {
  234 + update.setAddSource(addSource);
  235 + }
  236 + friendMapper.updateById(update);
156 237 return;
157 238 }
158   - // 情况二:新增好友关系
  239 + // 情况二:不存在记录 → 直接插入新记录
159 240 ImFriendDO friend = ImFriendDO.builder().userId(userId).friendUserId(friendUserId)
160   - .muted(false).status(CommonStatusEnum.ENABLE.getStatus()).addTime(LocalDateTime.now()).build();
161   - try {
162   - imFriendMapper.insert(friend);
163   - } catch (DuplicateKeyException e) {
164   - // 并发场景:另一个请求已先一步插入,且其插入的必然是 ENABLE 状态(DISABLE 场景在上方分支已处理),直接忽略
165   - log.warn("[addFriend0][userId({}) friendUserId({}) 并发插入冲突,忽略]", userId, friendUserId);
166   - }
  241 + .muted(false).pinned(false).blocked(false)
  242 + .displayName(displayName).addSource(addSource)
  243 + .status(CommonStatusEnum.ENABLE.getStatus()).addTime(LocalDateTime.now()).build();
  244 + friendMapper.insert(friend);
167 245 }
168 246  
169 247 /**
170 248 * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime)
  249 + * <p>
  250 + * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置
171 251 */
172   - @CacheEvict(cacheNames = FRIEND, key = "#userId + '_' + #friendUserId")
  252 + @Caching(evict = {
  253 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  254 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  255 + })
173 256 public void deleteFriend0(Long userId, Long friendUserId) {
174   - ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  257 + ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
175 258 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
176 259 return;
177 260 }
178   - imFriendMapper.updateById(new ImFriendDO().setId(exists.getId())
  261 + friendMapper.updateById(new ImFriendDO().setId(exists.getId())
179 262 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now()));
180 263 }
181 264  
... ... @@ -187,7 +270,7 @@ public class ImFriendServiceImpl implements ImFriendService {
187 270  
188 271 @Override
189 272 public PageResult<ImFriendDO> getFriendPage(ImFriendManagerPageReqVO reqVO) {
190   - return imFriendMapper.selectPage(reqVO);
  273 + return friendMapper.selectPage(reqVO);
191 274 }
192 275  
193 276 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
13 13 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
14 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
15 15 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
16 17 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
17 18 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
18 19 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
... ... @@ -66,9 +67,13 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
66 67 return existing;
67 68 }
68 69 // 1.2 好友校验
69   - if (!friendService.isFriend(senderId, reqVO.getReceiverId())) {
  70 + ImFriendStateEnum state = friendService.getFriendState(senderId, reqVO.getReceiverId());
  71 + if (state == ImFriendStateEnum.NONE) {
70 72 throw exception(FRIEND_NOT_FRIEND);
71 73 }
  74 + if (state == ImFriendStateEnum.BLOCKED) {
  75 + throw exception(FRIEND_BLOCKED_BY_PEER);
  76 + }
72 77 // 1.3 文本消息敏感词过滤
73 78 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) {
74 79 sensitiveWordService.validateText(reqVO.getContent());
... ... @@ -81,7 +86,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
81 86 .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()));
82 87 privateMessageMapper.insert(message);
83 88  
84   - // 3. WebSocket 异步推送(接收方 + 发送方多端同步)
  89 + // 3. WebSocket 异步推送:接收方 + 发送方多端同步
85 90 ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
86 91 imWebSocketService.sendPrivateMessageAsync(message.getReceiverId(), websocketMessage);
87 92 imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage);
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
1 1 package cn.iocoder.yudao.module.im.service.message.dto;
2 2  
3 3 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
4   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*;
  4 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.*;
5 5 import jakarta.validation.constraints.NotNull;
6 6 import lombok.Data;
7 7  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java
1 1 package cn.iocoder.yudao.module.im.service.websocket.dto;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 6 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  7 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.BaseFriendNotification;
6 8 import lombok.Data;
7 9 import lombok.experimental.Accessors;
8 10  
... ... @@ -103,45 +105,19 @@ public class ImPrivateMessageDTO {
103 105 // ==================== 好友变更相关 ====================
104 106  
105 107 /**
106   - * 构建好友添加推送 DTO(通知对方"你有一位新好友")
  108 + * 构建好友通知推送 DTO(统一入口)
107 109 *
108   - * @param userId 当前用户编号(即发起添加的用户)
109   - * @param friendUserId 好友的用户编号(即接收通知的用户)
  110 + * @param type 消息类型;取自 {@link ImMessageTypeEnum} 中的 FRIEND_* 段
  111 + * @param operatorUserId 操作人用户编号;同时作为帧的 senderId 用于定位接收端 friendUserId
  112 + * @param receiverUserId 推送目标用户编号
  113 + * @param payload 好友通知 payload(继承 {@link BaseFriendNotification})
110 114 * @return 私聊 DTO
111 115 */
112   - public static ImPrivateMessageDTO ofFriendAdd(Long userId, Long friendUserId) {
113   - return new ImPrivateMessageDTO()
114   - .setType(ImMessageTypeEnum.FRIEND_ADD.getType())
115   - .setSenderId(userId).setReceiverId(friendUserId)
116   - .setSendTime(LocalDateTime.now());
117   - }
118   -
119   - /**
120   - * 构建好友删除推送 DTO(通知对方"好友关系已解除")
121   - *
122   - * @param userId 当前用户编号(即发起删除的用户)
123   - * @param friendUserId 好友的用户编号(即接收通知的用户)
124   - * @return 私聊 DTO
125   - */
126   - public static ImPrivateMessageDTO ofFriendDelete(Long userId, Long friendUserId) {
127   - return new ImPrivateMessageDTO()
128   - .setType(ImMessageTypeEnum.FRIEND_DELETE.getType())
129   - .setSenderId(userId).setReceiverId(friendUserId)
130   - .setSendTime(LocalDateTime.now());
131   - }
132   -
133   - /**
134   - * 构建好友更新推送 DTO(通知自己多端同步好友属性变更)
135   - *
136   - * @param userId 当前用户编号
137   - * @param friendUserId 好友的用户编号
138   - * @return 私聊 DTO
139   - */
140   - public static ImPrivateMessageDTO ofFriendUpdate(Long userId, Long friendUserId) {
141   - return new ImPrivateMessageDTO()
142   - .setType(ImMessageTypeEnum.FRIEND_UPDATE.getType())
143   - .setSenderId(userId).setReceiverId(friendUserId)
144   - .setSendTime(LocalDateTime.now());
  116 + public static ImPrivateMessageDTO ofFriendNotification(Integer type, Long operatorUserId,
  117 + Long receiverUserId, BaseFriendNotification payload) {
  118 + return new ImPrivateMessageDTO().setType(type)
  119 + .setSenderId(operatorUserId).setReceiverId(receiverUserId)
  120 + .setContent(JsonUtils.toJsonString(payload)).setSendTime(LocalDateTime.now());
145 121 }
146 122  
147 123 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/BaseFriendNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 好友事件通知基类
  7 + * <p>
  8 + * 所有好友事件 payload 共享:
  9 + * - operatorUserId 标识"谁触发的",用于多端同步时本端识别"是我自己触发的"还是"对方触发的"
  10 + * - friendUserId 标识"好友是谁",前端按它定位 / 更新本地 friend 缓存
  11 + */
  12 +@Data
  13 +public abstract class BaseFriendNotification {
  14 +
  15 + /**
  16 + * 操作人用户编号
  17 + */
  18 + private Long operatorUserId;
  19 + /**
  20 + * 好友用户编号
  21 + */
  22 + private Long friendUserId;
  23 +
  24 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendAddNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 新增好友通知
  7 + * <p>
  8 + * 双向建立好友关系后,推送给 A、B 双方多端;前端拉取好友信息入库
  9 + */
  10 +@Data
  11 +public class FriendAddNotification extends BaseFriendNotification {
  12 +
  13 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendBlockNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 加入黑名单通知
  7 + * <p>
  8 + * A 拉黑 B 后仅推 A 多端;B 端不感知(B 那边 blocked=0 不变)
  9 + */
  10 +@Data
  11 +public class FriendBlockNotification extends BaseFriendNotification {
  12 +
  13 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 好友删除通知
  7 + * <p>
  8 + * 仅推送给操作人多端做同步(对端不感知,与单边删除语义对齐);前端清除本地好友 + 级联清理私聊会话
  9 + */
  10 +@Data
  11 +public class FriendDeleteNotification extends BaseFriendNotification {
  12 +
  13 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendInfoUpdatedNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +// TODO @AI:1209 暂未实现;好友信息在 system 模块,需要 system 改昵称 / 头像后发 ApplicationEvent,IM 模块监听后回调发送此通知
  6 +/**
  7 + * 好友资料变更通知(对端改了昵称 / 头像)
  8 + * <p>
  9 + * 推送给资料被改的人的所有好友;前端清缓存 + 重拉资料
  10 + * <p>
  11 + * 此处 friendUserId 表示「资料被更新的那个人」(即对端),与 operatorUserId 一致
  12 + */
  13 +@Data
  14 +public class FriendInfoUpdatedNotification extends BaseFriendNotification {
  15 +
  16 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestApprovedNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 好友申请被同意通知
  7 + * <p>
  8 + * 推送给原申请发起方多端;前端按 requestId 把对应申请记录 handleResult 更新为「已同意」
  9 + */
  10 +@Data
  11 +public class FriendRequestApprovedNotification extends BaseFriendNotification {
  12 +
  13 + /**
  14 + * 已处理的申请记录编号
  15 + */
  16 + private Long requestId;
  17 +
  18 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 收到新的好友申请通知
  7 + * <p>
  8 + * 推送给申请的接收方多端;前端拿到 requestId 后按需 fetch 完整申请记录,避免 payload 体积过大
  9 + */
  10 +@Data
  11 +public class FriendRequestNotification extends BaseFriendNotification {
  12 +
  13 + /**
  14 + * 申请记录编号
  15 + */
  16 + private Long requestId;
  17 + /**
  18 + * 申请理由
  19 + */
  20 + private String applyContent;
  21 + /**
  22 + * 添加来源
  23 + */
  24 + private Integer addSource;
  25 +
  26 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestRejectedNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 好友申请被拒绝通知
  7 + * <p>
  8 + * 推送给原申请发起方多端;前端按 requestId 把对应申请记录 handleResult 更新为「已拒绝」
  9 + */
  10 +@Data
  11 +public class FriendRequestRejectedNotification extends BaseFriendNotification {
  12 +
  13 + /**
  14 + * 已处理的申请记录编号
  15 + */
  16 + private Long requestId;
  17 + /**
  18 + * 拒绝理由(可选)
  19 + */
  20 + private String handleContent;
  21 +
  22 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendUnblockNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 移出黑名单通知
  7 + * <p>
  8 + * A 把 B 移出黑名单后仅推 A 多端
  9 + */
  10 +@Data
  11 +public class FriendUnblockNotification extends BaseFriendNotification {
  12 +
  13 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 好友信息批量更新通知(备注 / 免打扰 / 联系人置顶等单边属性)
  7 + * <p>
  8 + * A 改了对 B 的备注 / 免打扰 / 置顶等单边属性后仅推 A 多端做同步;
  9 + * 一次 update 涉及多个字段时合并为单条通知,避免多通知顺序竞争。
  10 + */
  11 +@Data
  12 +public class FriendUpdateNotification extends BaseFriendNotification {
  13 +
  14 + /**
  15 + * 备注;不为空则更新(空串表示清空)
  16 + */
  17 + private String displayName;
  18 + /**
  19 + * 免打扰;不为空则更新
  20 + */
  21 + private Boolean muted;
  22 + /**
  23 + * 联系人置顶;不为空则更新
  24 + */
  25 + private Boolean pinned;
  26 +
  27 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/BaseGroupNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 添加管理员事件通知(memberUserIds 为被设管理员的成员)
5 5 */
6   -public class GroupAdminAddNotification extends MembersAffectedNotification {
  6 +public class GroupAdminAddNotification extends GroupMembersAffectedNotification {
7 7 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 撤销管理员事件通知(memberUserIds 为被撤销管理员的成员)
5 5 */
6   -public class GroupAdminRemoveNotification extends MembersAffectedNotification {
  6 +public class GroupAdminRemoveNotification extends GroupMembersAffectedNotification {
7 7 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 群创建事件通知(memberUserIds 含创建者 + 初始邀请成员)
5 5 */
6   -public class GroupCreateNotification extends MembersAffectedNotification {
  6 +public class GroupCreateNotification extends GroupMembersAffectedNotification {
7 7 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupDissolveNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 群解散事件通知
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupInfoUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 成员加入事件通知(memberUserIds 为被邀请人)
5 5 */
6   -public class GroupMemberInviteNotification extends MembersAffectedNotification {
  6 +public class GroupMemberInviteNotification extends GroupMembersAffectedNotification {
7 7 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 成员被移出事件通知(memberUserIds 为被移出成员)
5 5 */
6   -public class GroupMemberKickNotification extends MembersAffectedNotification {
  6 +public class GroupMemberKickNotification extends GroupMembersAffectedNotification {
7 7 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberNicknameUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberQuitNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 /**
4 4 * 成员退群事件通知
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberSettingUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMembersAffectedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
5 5 import java.util.List;
6 6  
  7 +// TODO DONE @AI:本身已是 BaseGroupNotification 的子类,名字不带 Group 前缀是因为是抽象基类、被多种 Group* 通知共用
  8 +// TODO @AI:它目前只被 group 使用。别过度设计,名字按照我说的调整下;
7 9 /**
8 10 * 涉及成员列表的群事件通知基类:用于 Create / Invite / Kick / AdminAdd / AdminRemove
9 11 */
10 12 @Data
11   -public abstract class MembersAffectedNotification extends BaseGroupNotification {
  13 +public abstract class GroupMembersAffectedNotification extends BaseGroupNotification {
12 14  
13 15 /**
14 16 * 受影响的成员用户编号列表
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessagePinNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
4 4 import lombok.Data;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessageUnpinNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessageUnpinNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupNameUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupNoticeUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupOwnerTransferNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/test/resources/sql/clean.sql
... ... @@ -3,3 +3,4 @@ DELETE FROM &quot;im_group_message&quot;;
3 3 DELETE FROM "im_group";
4 4 DELETE FROM "im_group_member";
5 5 DELETE FROM "im_friend";
  6 +DELETE FROM "im_friend_request";
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -84,6 +84,9 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend&quot; (
84 84 "friend_user_id" bigint NOT NULL COMMENT '好友用户编号',
85 85 "muted" bit DEFAULT FALSE COMMENT '是否免打扰',
86 86 "display_name" varchar(64) NOT NULL DEFAULT '' COMMENT '好友展示备注(仅自己可见)',
  87 + "add_source" tinyint DEFAULT NULL COMMENT '添加来源',
  88 + "pinned" bit DEFAULT FALSE COMMENT '是否置顶联系人',
  89 + "blocked" bit DEFAULT FALSE COMMENT '是否拉黑',
87 90 "status" tinyint NOT NULL COMMENT '好友状态',
88 91 "add_time" timestamp DEFAULT NULL COMMENT '添加好友时间',
89 92 "delete_time" timestamp DEFAULT NULL COMMENT '删除好友时间',
... ... @@ -95,3 +98,22 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend&quot; (
95 98 "tenant_id" bigint NOT NULL DEFAULT 0,
96 99 PRIMARY KEY ("id")
97 100 ) COMMENT 'IM 好友关系表';
  101 +
  102 +CREATE TABLE IF NOT EXISTS "im_friend_request" (
  103 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  104 + "from_user_id" bigint NOT NULL COMMENT '发起方用户编号',
  105 + "to_user_id" bigint NOT NULL COMMENT '接收方用户编号',
  106 + "handle_result" tinyint NOT NULL DEFAULT 0 COMMENT '处理结果;0未处理;1同意;2拒绝',
  107 + "apply_content" varchar(255) DEFAULT NULL COMMENT '申请理由',
  108 + "handle_content" varchar(255) DEFAULT NULL COMMENT '处理理由',
  109 + "display_name" varchar(64) DEFAULT NULL COMMENT '发起方对接收方的备注',
  110 + "add_source" tinyint DEFAULT NULL COMMENT '添加来源',
  111 + "handle_time" timestamp DEFAULT NULL COMMENT '处理时间',
  112 + "creator" varchar(64) DEFAULT '',
  113 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  114 + "updater" varchar(64) DEFAULT '',
  115 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  116 + "deleted" bit NOT NULL DEFAULT FALSE,
  117 + "tenant_id" bigint NOT NULL DEFAULT 0,
  118 + PRIMARY KEY ("id")
  119 +) COMMENT 'IM 好友申请记录表';
... ...