Commit affc409022d066c4b53f8fcf33eb7690383bce04

Authored by YunaiV
1 parent 758dfcbb

✨ feat(friend): 新增 IM 好友管理功能,包括添加、删除和更新好友信息

实现了 IM 好友的增删改查功能,支持免打扰状态的更新。新增了相关的请求和响应对象,优化了好友列表的获取逻辑,并确保了好友关系的有效性校验。

- 新增 ImFriendRespVO 和 ImFriendUpdateReqVO 类
- 更新 ImFriendController,添加更新好友信息接口
- 实现 ImFriendService 中的好友管理逻辑
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
1 package cn.iocoder.yudao.module.im.controller.admin.friend; 1 package cn.iocoder.yudao.module.im.controller.admin.friend;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;  
5 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO; 4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
6 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 6 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
7 import io.swagger.v3.oas.annotations.Operation; 7 import io.swagger.v3.oas.annotations.Operation;
8 import io.swagger.v3.oas.annotations.Parameter; 8 import io.swagger.v3.oas.annotations.Parameter;
9 import io.swagger.v3.oas.annotations.tags.Tag; 9 import io.swagger.v3.oas.annotations.tags.Tag;
10 import jakarta.annotation.Resource; 10 import jakarta.annotation.Resource;
  11 +import jakarta.validation.Valid;
11 import jakarta.validation.constraints.NotNull; 12 import jakarta.validation.constraints.NotNull;
12 import org.springframework.validation.annotation.Validated; 13 import org.springframework.validation.annotation.Validated;
13 import org.springframework.web.bind.annotation.*; 14 import org.springframework.web.bind.annotation.*;
@@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.*; @@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.*;
15 import java.util.List; 16 import java.util.List;
16 17
17 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 18 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  19 +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
18 20
19 @Tag(name = "管理后台 - IM 好友") 21 @Tag(name = "管理后台 - IM 好友")
20 @RestController 22 @RestController
@@ -28,16 +30,14 @@ public class ImFriendController { @@ -28,16 +30,14 @@ public class ImFriendController {
28 @GetMapping("/list") 30 @GetMapping("/list")
29 @Operation(summary = "获得当前登录用户的好友列表") 31 @Operation(summary = "获得当前登录用户的好友列表")
30 public CommonResult<List<ImFriendRespVO>> getMyFriendList() { 32 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
31 - Long userId = SecurityFrameworkUtils.getLoginUserId();  
32 - return success(friendService.getMyFriendList(userId)); 33 + return success(friendService.getMyFriendList(getLoginUserId()));
33 } 34 }
34 35
35 @GetMapping("/get") 36 @GetMapping("/get")
36 @Operation(summary = "获得好友详情") 37 @Operation(summary = "获得好友详情")
37 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") 38 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
38 public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) { 39 public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) {
39 - Long userId = SecurityFrameworkUtils.getLoginUserId();  
40 - return success(friendService.getFriend(userId, friendUserId)); 40 + return success(friendService.getFriend(getLoginUserId(), friendUserId));
41 } 41 }
42 42
43 @PostMapping("/add") 43 @PostMapping("/add")
@@ -45,8 +45,7 @@ public class ImFriendController { @@ -45,8 +45,7 @@ public class ImFriendController {
45 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") 45 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
46 public CommonResult<Boolean> addFriend( 46 public CommonResult<Boolean> addFriend(
47 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) { 47 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
48 - Long userId = SecurityFrameworkUtils.getLoginUserId();  
49 - friendService.addFriend(userId, friendUserId); 48 + friendService.addFriend(getLoginUserId(), friendUserId);
50 return success(true); 49 return success(true);
51 } 50 }
52 51
@@ -55,11 +54,15 @@ public class ImFriendController { @@ -55,11 +54,15 @@ public class ImFriendController {
55 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") 54 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
56 public CommonResult<Boolean> deleteFriend( 55 public CommonResult<Boolean> deleteFriend(
57 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) { 56 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
58 - Long userId = SecurityFrameworkUtils.getLoginUserId();  
59 - friendService.deleteFriend(userId, friendUserId); 57 + friendService.deleteFriend(getLoginUserId(), friendUserId);
60 return success(true); 58 return success(true);
61 } 59 }
62 60
63 - // TODO @AI:增加一个 updateFriend 接口,目前暂时就 dnd(免打扰)功能,后续可以增加备注等功能 61 + @PutMapping("/update")
  62 + @Operation(summary = "更新好友信息(当前仅免打扰)")
  63 + public CommonResult<Boolean> updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
  64 + friendService.updateFriend(getLoginUserId(), reqVO);
  65 + return success(true);
  66 + }
64 67
65 } 68 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
  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 ImFriendRespVO {
  16 +
  17 + @Schema(description = "关系记录编号", example = "1024")
  18 + private Long id;
  19 +
  20 + @Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  21 + private Long friendUserId;
  22 +
  23 + @Schema(description = "是否免打扰", example = "false")
  24 + private Boolean muted;
  25 +
  26 + @Schema(description = "好友状态(0=正常,1=已删除)", example = "0")
  27 + private Integer status;
  28 +
  29 + @Schema(description = "添加好友时间")
  30 + private LocalDateTime addTime;
  31 +
  32 + @Schema(description = "删除好友时间")
  33 + private LocalDateTime deleteTime;
  34 +
  35 + // ========== 下面是聚合字段,方便前端显示 ==========
  36 +
  37 + @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道")
  38 + private String nickname;
  39 +
  40 + @Schema(description = "好友头像(实时聚合自 AdminUser)")
  41 + private String avatar;
  42 +
  43 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - IM 好友更新 Request VO")
  8 +@Data
  9 +public class ImFriendUpdateReqVO {
  10 +
  11 + @Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  12 + @NotNull(message = "好友用户编号不能为空")
  13 + private Long friendUserId;
  14 +
  15 + @Schema(description = "是否免打扰", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  16 + @NotNull(message = "免打扰状态不能为空")
  17 + private Boolean muted;
  18 +
  19 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java
1 package cn.iocoder.yudao.module.im.dal.dataobject.friend; 1 package cn.iocoder.yudao.module.im.dal.dataobject.friend;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 import com.baomidou.mybatisplus.annotation.KeySequence; 5 import com.baomidou.mybatisplus.annotation.KeySequence;
5 import com.baomidou.mybatisplus.annotation.TableId; 6 import com.baomidou.mybatisplus.annotation.TableId;
6 import com.baomidou.mybatisplus.annotation.TableName; 7 import com.baomidou.mybatisplus.annotation.TableName;
7 import lombok.*; 8 import lombok.*;
8 9
  10 +import java.time.LocalDateTime;
  11 +
9 /** 12 /**
10 * IM 好友关系 DO 13 * IM 好友关系 DO
11 * <p> 14 * <p>
12 * 业务语义: 15 * 业务语义:
13 * - 双向关系:A-B 互为好友会存 2 条记录(userId=A, friendUserId=B 和 userId=B, friendUserId=A) 16 * - 双向关系:A-B 互为好友会存 2 条记录(userId=A, friendUserId=B 和 userId=B, friendUserId=A)
14 - * - 软删除:`deleted=1` 表示单向解除好友;如果需要"全部不看到",双向都要 deleted=1  
15 - * - 免打扰:`muted` 控制是否屏蔽来自该好友的通知 17 + * - 状态管理:{@link #status} 使用 {@link CommonStatusEnum},ENABLE=正常,DISABLE=已删除
  18 + * - 免打扰:{@link #muted} 控制是否屏蔽来自该好友的通知
16 * 19 *
17 * @author 芋道源码 20 * @author 芋道源码
18 */ 21 */
@@ -47,12 +50,21 @@ public class ImFriendDO extends BaseDO { @@ -47,12 +50,21 @@ public class ImFriendDO extends BaseDO {
47 * 是否免打扰 50 * 是否免打扰
48 */ 51 */
49 private Boolean muted; 52 private Boolean muted;
50 -  
51 - // TODO @AI:status 字段,枚举 CommonStatusEnum,表示正常 / 解散;如果是解散状态,则不允许任何操作了。  
52 - // TODO @AI:addTime、deleteTime 字段,分别表示添加好友的时间和删除好友的时间;如果 deleteTime 不为 null,则表示已删除。不使用 deleted;  
53 -  
54 - // 说明:  
55 - // - 删除状态用 BaseDO 的 deleted 字段表达(0=正常,1=已删除)  
56 - // - 添加时间用 BaseDO 的 createTime;删除时间对应 updateTime 53 + /**
  54 + * 好友状态
  55 + * <p>
  56 + * 枚举 {@link CommonStatusEnum}
  57 + */
  58 + private Integer status;
  59 + /**
  60 + * 添加好友时间
  61 + */
  62 + private LocalDateTime addTime;
  63 + /**
  64 + * 删除好友时间
  65 + * <p>
  66 + * 不为 null 时表示已删除
  67 + */
  68 + private LocalDateTime deleteTime;
57 69
58 } 70 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
  3 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
  4 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
  5 +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
  6 +
  7 +import java.util.List;
  8 +
3 /** 9 /**
4 * IM 好友关系 Service 接口 10 * IM 好友关系 Service 接口
5 * 11 *
@@ -9,12 +15,75 @@ public interface ImFriendService { @@ -9,12 +15,75 @@ public interface ImFriendService {
9 15
10 /** 16 /**
11 * 校验两个用户是否是好友关系(未删除状态) 17 * 校验两个用户是否是好友关系(未删除状态)
  18 + *
  19 + * @param userId 用户编号
  20 + * @param friendUserId 好友用户编号
  21 + */
  22 + void validateFriendExists(Long userId, Long friendUserId);
  23 +
  24 + /**
  25 + * 获得当前用户的好友列表(含昵称/头像聚合)
  26 + * <p>
  27 + * 返回的列表里**包含已删除状态**,前端按 status 区分;
  28 + * 原因是本地已经建立会话的好友被单向删除后,前端仍需要展示头像/昵称。
  29 + *
  30 + * @param userId 用户编号
  31 + * @return 好友列表(含聚合后的用户信息)
  32 + */
  33 + List<ImFriendRespVO> getMyFriendList(Long userId);
  34 +
  35 + /**
  36 + * 查询一个好友(仅限本人视角的好友关系)
  37 + * <p>
  38 + * 只返回"本人→对方"的关系记录;
  39 + * 对方不是本人好友时抛 {@link cn.iocoder.yudao.module.im.enums.ErrorCodeConstants#FRIEND_NOT_FRIEND}。
  40 + *
  41 + * @param userId 当前登录用户编号
  42 + * @param friendUserId 目标用户编号
  43 + * @return 好友信息(含聚合后的用户信息)
  44 + */
  45 + ImFriendRespVO getFriend(Long userId, Long friendUserId);
  46 +
  47 + /**
  48 + * 添加好友(双向绑定)
  49 + * <p>
  50 + * 核心流程:
  51 + * 1. 校验:不允许添加自己;对方必须存在
  52 + * 2. 双向插入/更新关系(若已存在则刷新为未删除)
  53 + * 3. 推送"你们已成为好友"系统提示(TODO)
  54 + *
  55 + * @param userId 当前登录用户编号
  56 + * @param friendUserId 好友的用户编号
  57 + */
  58 + void addFriend(Long userId, Long friendUserId);
  59 +
  60 + /**
  61 + * 删除好友(双向软删除)
  62 + * <p>
  63 + * 两边关系都标记为已删除状态。
  64 + *
  65 + * @param userId 当前登录用户编号
  66 + * @param friendUserId 好友的用户编号
  67 + */
  68 + void deleteFriend(Long userId, Long friendUserId);
  69 +
  70 + /**
  71 + * 更新好友信息(仅更新本人→对方的单边关系属性)
12 * <p> 72 * <p>
13 - * 如果不是好友,直接抛出业务异常 73 + * 目前支持:免打扰(muted);后续可扩展备注等字段。
  74 + *
  75 + * @param userId 当前登录用户编号
  76 + * @param reqVO 更新请求
  77 + */
  78 + void updateFriend(Long userId, ImFriendUpdateReqVO reqVO);
  79 +
  80 + /**
  81 + * 查询某个用户视角下的好友关系记录(仅单边)
14 * 82 *
15 * @param userId 用户编号 83 * @param userId 用户编号
16 * @param friendUserId 好友用户编号 84 * @param friendUserId 好友用户编号
  85 + * @return 关系记录,不存在返回 null
17 */ 86 */
18 - void validateIsFriend(Long userId, Long friendUserId); 87 + ImFriendDO getFriendDO(Long userId, Long friendUserId);
19 88
20 } 89 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  5 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
  6 +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
5 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; 8 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
  9 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  10 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
6 import jakarta.annotation.Resource; 11 import jakarta.annotation.Resource;
7 import org.springframework.stereotype.Service; 12 import org.springframework.stereotype.Service;
  13 +import org.springframework.transaction.annotation.Transactional;
8 import org.springframework.validation.annotation.Validated; 14 import org.springframework.validation.annotation.Validated;
9 15
  16 +import java.time.LocalDateTime;
  17 +import java.util.Collections;
  18 +import java.util.List;
  19 +import java.util.Map;
  20 +import java.util.Objects;
  21 +
10 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 22 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
11 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; 24 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
12 25
13 /** 26 /**
@@ -22,12 +35,160 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -22,12 +35,160 @@ public class ImFriendServiceImpl implements ImFriendService {
22 @Resource 35 @Resource
23 private ImFriendMapper imFriendMapper; 36 private ImFriendMapper imFriendMapper;
24 37
  38 + @Resource
  39 + private AdminUserApi adminUserApi;
  40 +
25 @Override 41 @Override
26 - public void validateIsFriend(Long userId, Long friendUserId) { 42 + public void validateFriendExists(Long userId, Long friendUserId) {
27 ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); 43 ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
28 - if (friend == null || CommonStatusEnum.DISABLE.getStatus().equals(friend.getStatus())) { 44 + if (friend == null || CommonStatusEnum.isDisable(friend.getStatus())) {
  45 + throw exception(FRIEND_NOT_FRIEND);
  46 + }
  47 + }
  48 +
  49 + // TODO @AI:这里只负责返回 List<ImFriendDO>;VO 组装在 controller 搞;
  50 + // TODO @AI:方法改成 getEnabledFriendList 之类的,明确只返回未删除的好友;如果需要展示已删除的好友,提供一个 getAllFriendList 之类的方法。
  51 + @Override
  52 + public List<ImFriendRespVO> getMyFriendList(Long userId) {
  53 + List<ImFriendDO> friends = imFriendMapper.selectListByUserId(userId);
  54 + if (friends.isEmpty()) {
  55 + return Collections.emptyList();
  56 + }
  57 + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
  58 + List<Long> friendUserIds = CollectionUtils.convertList(friends, ImFriendDO::getFriendUserId);
  59 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(friendUserIds);
  60 + return CollectionUtils.convertList(friends, f -> buildRespVO(f, userMap.get(f.getFriendUserId())));
  61 + }
  62 +
  63 + // TODO @AI:这里只负责返回 ImFriendDO;
  64 + @Override
  65 + public ImFriendRespVO getFriend(Long userId, Long friendUserId) {
  66 + ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  67 + if (friend == null) {
  68 + throw exception(FRIEND_NOT_FRIEND);
  69 + }
  70 + AdminUserRespDTO user = adminUserApi.getUser(friendUserId);
  71 + return buildRespVO(friend, user);
  72 + }
  73 +
  74 + @Override
  75 + public ImFriendDO getFriendDO(Long userId, Long friendUserId) {
  76 + return imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  77 + }
  78 +
  79 + @Override
  80 + public void updateFriend(Long userId, ImFriendUpdateReqVO reqVO) {
  81 + // 1. 校验好友关系存在
  82 + ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, reqVO.getFriendUserId());
  83 + if (friend == null) {
29 throw exception(FRIEND_NOT_FRIEND); 84 throw exception(FRIEND_NOT_FRIEND);
30 } 85 }
  86 + // 2. 更新好友属性(目前仅免打扰)
  87 + ImFriendDO updateObj = new ImFriendDO();
  88 + updateObj.setId(friend.getId());
  89 + updateObj.setMuted(reqVO.getMuted());
  90 + imFriendMapper.updateById(updateObj);
  91 + }
  92 +
  93 + @Override
  94 + @Transactional(rollbackFor = Exception.class)
  95 + public void addFriend(Long userId, Long friendUserId) {
  96 + // 1.1 校验:不允许添加自己为好友
  97 + if (Objects.equals(userId, friendUserId)) {
  98 + throw exception(FRIEND_ADD_SELF);
  99 + }
  100 + // 1.2 校验对方存在(禁用/不存在的账号不允许添加)
  101 + adminUserApi.validateUser(friendUserId);
  102 +
  103 + // 双向绑定
  104 + // DONE @AI:改成 createOrUpdateFriend 之类的接口,内部自动处理"已删除的好友再加回来"的情况;或者直接在数据库层面通过唯一索引 + 物理更新来解决。
  105 + // 说明:bindFriend 内部已处理 DISABLE → ENABLE 的恢复逻辑
  106 + bindFriend(userId, friendUserId);
  107 + bindFriend(friendUserId, userId);
  108 +
  109 + // TODO @AI:推送异步化:1)好友的打招呼;2)通知好友发生变化的消息;
  110 +
  111 + // TODO @AI:实现如下:
  112 + // TODO @芋艿:【对齐】这里缺少了好友添加成功的系统提示消息,
  113 + // 需要通过 `ImPrivateMessageService` 插入一条 TIP_TEXT 类型的消息并走 WebSocket 推送给双方。
  114 + // 涉及 WebSocket 推送器的事件类型扩展(当前 `ImWsEventType` 无 FRIEND_NEW),待统一后补上。
  115 + }
  116 +
  117 + @Override
  118 + @Transactional(rollbackFor = Exception.class)
  119 + public void deleteFriend(Long userId, Long friendUserId) {
  120 + // 双向标记为 DISABLE + 记录 deleteTime
  121 + unbindFriend(userId, friendUserId);
  122 + unbindFriend(friendUserId, userId);
  123 +
  124 + // TODO @芋艿:【对齐】这里缺少了好友删除的系统提示 + FRIEND_DEL WebSocket 推送,
  125 + // 待 WS 事件类型扩展后补上。
  126 + }
  127 +
  128 + /**
  129 + * 单向绑定好友关系:
  130 + * - 首次:新增记录(status=ENABLE,addTime=now)
  131 + * - 已存在且 ENABLE:无需重复操作
  132 + * - 已存在但 DISABLE:恢复关系(status=ENABLE,刷新 addTime,清空 deleteTime)
  133 + */
  134 + // TODO @AI:spring cache;参考别的模块的写法;
  135 + private void bindFriend(Long userId, Long friendUserId) {
  136 + ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  137 + if (exists != null) {
  138 + if (CommonStatusEnum.isEnable(exists.getStatus())) {
  139 + return; // 已是好友,跳过
  140 + }
  141 + // 恢复已删除的好友关系
  142 + ImFriendDO updateObj = new ImFriendDO();
  143 + updateObj.setId(exists.getId());
  144 + updateObj.setStatus(CommonStatusEnum.ENABLE.getStatus());
  145 + updateObj.setAddTime(LocalDateTime.now());
  146 + updateObj.setDeleteTime(null);
  147 + imFriendMapper.updateById(updateObj);
  148 + return;
  149 + }
  150 + // 新增
  151 + ImFriendDO friend = ImFriendDO.builder()
  152 + .userId(userId)
  153 + .friendUserId(friendUserId)
  154 + .muted(false)
  155 + .status(CommonStatusEnum.ENABLE.getStatus())
  156 + .addTime(LocalDateTime.now())
  157 + .build();
  158 + imFriendMapper.insert(friend);
  159 + }
  160 +
  161 + /**
  162 + * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime)
  163 + */
  164 + private void unbindFriend(Long userId, Long friendUserId) {
  165 + ImFriendDO exists = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
  166 + if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
  167 + return;
  168 + }
  169 + ImFriendDO updateObj = new ImFriendDO();
  170 + updateObj.setId(exists.getId());
  171 + updateObj.setStatus(CommonStatusEnum.DISABLE.getStatus());
  172 + updateObj.setDeleteTime(LocalDateTime.now());
  173 + imFriendMapper.updateById(updateObj);
  174 + }
  175 +
  176 + /**
  177 + * 聚合 ImFriendDO + AdminUser 构造响应 VO
  178 + */
  179 + private ImFriendRespVO buildRespVO(ImFriendDO friend, AdminUserRespDTO user) {
  180 + ImFriendRespVO vo = new ImFriendRespVO();
  181 + vo.setId(friend.getId());
  182 + vo.setFriendUserId(friend.getFriendUserId());
  183 + vo.setMuted(friend.getMuted());
  184 + vo.setStatus(friend.getStatus());
  185 + vo.setAddTime(friend.getAddTime());
  186 + vo.setDeleteTime(friend.getDeleteTime());
  187 + if (user != null) {
  188 + vo.setNickname(user.getNickname());
  189 + vo.setAvatar(user.getAvatar());
  190 + }
  191 + return vo;
31 } 192 }
32 193
33 } 194 }