Commit f0be7ba137481fc69b5c5927be8921285a0d918a
1 parent
e266a0fc
✨ feat(im): 优化好友请求处理逻辑,增加乐观锁支持
Showing
4 changed files
with
52 additions
and
28 deletions
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 "im_friend" ( |
| 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" ( | ... | ... |