Commit f0be7ba137481fc69b5c5927be8921285a0d918a

Authored by YunaiV
1 parent e266a0fc

✨ feat(im): 优化好友请求处理逻辑,增加乐观锁支持

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend;
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
  6 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
6 7 import org.apache.ibatis.annotations.Mapper;
7 8  
8 9 import java.util.List;
... ... @@ -33,4 +34,9 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> {
33 34 return selectList(wrapper);
34 35 }
35 36  
  37 + default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) {
  38 + return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>()
  39 + .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult));
  40 + }
  41 +
36 42 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
... ... @@ -121,14 +121,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
121 121 @Override
122 122 @Transactional(rollbackFor = Exception.class)
123 123 public void agreeFriendRequest(Long userId, Long requestId) {
124   - // 1. 校验申请存在 + 未处理 + 操作人是接收方
  124 + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底)
125 125 ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
126 126  
127   - // 2. 更新申请:handleResult=AGREED + handleTime
128   - LocalDateTime now = LocalDateTime.now();
129   - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId())
130   - .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now));
131   - request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now);
  127 + // 2. 乐观锁更新申请:handleResult=AGREED + handleTime;并发同意会有一方 affectedRows=0
  128 + ImFriendRequestDO updateObj = new ImFriendRequestDO()
  129 + .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(LocalDateTime.now());
  130 + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
  131 + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
  132 + if (affected == 0) {
  133 + throw exception(FRIEND_REQUEST_HANDLED);
  134 + }
  135 + request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(updateObj.getHandleTime());
132 136  
133 137 // 3. 双向建立好友关系
134 138 friendService.becomeFriends(request);
... ... @@ -144,14 +148,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
144 148 @Override
145 149 @Transactional(rollbackFor = Exception.class)
146 150 public void refuseFriendRequest(Long userId, Long requestId, String handleContent) {
147   - // 1. 校验申请存在 + 未处理 + 操作人是接收方
  151 + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底)
148 152 ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
149 153  
150   - // 2. 更新申请:handleResult=REFUSED + handleContent + handleTime
151   - LocalDateTime now = LocalDateTime.now();
152   - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId())
  154 + // 2. 乐观锁更新申请:handleResult=REFUSED + handleContent + handleTime;并发拒绝会有一方 affectedRows=0
  155 + ImFriendRequestDO updateObj = new ImFriendRequestDO()
153 156 .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult())
154   - .setHandleContent(handleContent).setHandleTime(now));
  157 + .setHandleContent(handleContent).setHandleTime(LocalDateTime.now());
  158 + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
  159 + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
  160 + if (affected == 0) {
  161 + throw exception(FRIEND_REQUEST_HANDLED);
  162 + }
155 163  
156 164 // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端
157 165 FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
... ... @@ -115,11 +115,16 @@ public class ImFriendServiceImpl implements ImFriendService {
115 115 }
116 116  
117 117 @Override
  118 + @Caching(evict = {
  119 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.fromUserId + '_' + #request.toUserId"),
  120 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.toUserId + '_' + #request.fromUserId")
  121 + })
118 122 @Transactional(rollbackFor = Exception.class)
119 123 public void becomeFriends(ImFriendRequestDO request) {
120 124 Long fromUserId = request.getFromUserId();
121 125 Long toUserId = request.getToUserId();
122 126 // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源
  127 + // FRIEND_STATE 双向失效由方法上的 @Caching 注解处理;framework 已开 transactionAware 自动延迟到 afterCommit
123 128 getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource());
124 129 getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource());
125 130  
... ... @@ -139,9 +144,13 @@ public class ImFriendServiceImpl implements ImFriendService {
139 144 }
140 145  
141 146 @Override
  147 + @Caching(evict = {
  148 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  149 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  150 + })
142 151 @Transactional(rollbackFor = Exception.class)
143 152 public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) {
144   - // 1. 单边重新启用我侧好友关系;addFriend0 内部已做 @CacheEvict 双向失效
  153 + // 1. 单边重新启用我侧好友关系
145 154 getSelf().addFriend0(userId, friendUserId, displayName, addSource);
146 155  
147 156 // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉)
... ... @@ -152,6 +161,10 @@ public class ImFriendServiceImpl implements ImFriendService {
152 161 }
153 162  
154 163 @Override
  164 + @Caching(evict = {
  165 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
  166 + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
  167 + })
155 168 @Transactional(rollbackFor = Exception.class)
156 169 public void deleteFriend(Long userId, Long friendUserId) {
157 170 // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动
... ... @@ -220,17 +233,15 @@ public class ImFriendServiceImpl implements ImFriendService {
220 233 }
221 234  
222 235 /**
223   - * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} 调用):
  236 + * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用):
224 237 * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置)
225 238 * - 情况二:不存在记录 → 直接插入新记录
226 239 * <p>
227   - * 并发安全:业务侧通过 friend_request.handle_result 已保证 agree 不会并发触发;
228   - * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。
  240 + * 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进;
  241 + * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。
  242 + * <p>
  243 + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
229 244 */
230   - @Caching(evict = {
231   - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
232   - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
233   - })
234 245 public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) {
235 246 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
236 247 // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐
... ... @@ -259,11 +270,9 @@ public class ImFriendServiceImpl implements ImFriendService {
259 270 * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime)
260 271 * <p>
261 272 * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置
  273 + * <p>
  274 + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict
262 275 */
263   - @Caching(evict = {
264   - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"),
265   - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId")
266   - })
267 276 public void deleteFriend0(Long userId, Long friendUserId) {
268 277 ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId);
269 278 if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) {
... ... @@ -273,10 +282,6 @@ public class ImFriendServiceImpl implements ImFriendService {
273 282 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now()));
274 283 }
275 284  
276   - private ImFriendServiceImpl getSelf() {
277   - return SpringUtil.getBean(getClass());
278   - }
279   -
280 285 // ==================== 管理后台 ====================
281 286  
282 287 @Override
... ... @@ -284,4 +289,8 @@ public class ImFriendServiceImpl implements ImFriendService {
284 289 return friendMapper.selectPage(reqVO);
285 290 }
286 291  
  292 + private ImFriendServiceImpl getSelf() {
  293 + return SpringUtil.getBean(getClass());
  294 + }
  295 +
287 296 }
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend&quot; (
96 96 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
97 97 "deleted" bit NOT NULL DEFAULT FALSE,
98 98 "tenant_id" bigint NOT NULL DEFAULT 0,
99   - PRIMARY KEY ("id")
  99 + PRIMARY KEY ("id"),
  100 + CONSTRAINT "uk_user_friend" UNIQUE ("user_id", "friend_user_id", "tenant_id")
100 101 ) COMMENT 'IM 好友关系表';
101 102  
102 103 CREATE TABLE IF NOT EXISTS "im_friend_request" (
... ...