huminmin
4 小时以前 a53f698e4667bc57dcc1de65f77936024f9a06a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package cn.iocoder.yudao.module.im.service.friend;
 
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerPageReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
import cn.iocoder.yudao.module.im.framework.config.ImProperties;
import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestApprovedNotification;
import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestNotification;
import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestRejectedNotification;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
 
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
 
/**
 * IM 好友申请 Service 实现类
 *
 * @author 芋道源码
 */
@Slf4j
@Service
@Validated
public class ImFriendRequestServiceImpl implements ImFriendRequestService {
 
    @Resource
    private ImFriendRequestMapper friendRequestMapper;
 
    @Resource
    private ImFriendService friendService;
    @Resource
    private ImWebSocketService websocketService;
 
    @Resource
    private ImProperties imProperties;
 
    @Resource
    private AdminUserApi adminUserApi;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ImFriendRequestDO applyFriend(Long fromUserId, ImFriendRequestApplyReqVO reqVO) {
        Long toUserId = reqVO.getToUserId();
        // 1.1 校验:不能加自己
        if (Objects.equals(fromUserId, toUserId)) {
            throw exception(FRIEND_ADD_SELF);
        }
        // 1.2 校验对方存在且启用
        adminUserApi.validateUser(toUserId);
        // 1.3 已是好友 / 被对方拉黑:直接报错(state 一次拿到双向状态,省两次单边查询)
        // 错误码与 ImFriendService#validateFriend 不同(语义为「申请被拒」),故保留 inline
        Integer state = friendService.getFriendState(fromUserId, toUserId);
        if (ImFriendStateEnum.isFriend(state)) {
            throw exception(FRIEND_REQUEST_ALREADY_FRIEND);
        }
        if (ImFriendStateEnum.isBlocked(state)) {
            throw exception(FRIEND_REQUEST_BLOCKED_BY_PEER);
        }
        // 1.4 单向好友(我已删 + 对方仍把我当好友):静默重新启用我侧关系,避免对方感知我曾删除
        ImFriendDO peerFriend = friendService.getFriend(toUserId, fromUserId);
        if (peerFriend != null && CommonStatusEnum.isEnable(peerFriend.getStatus())) {
            // 对方已拉黑:静默恢复等于绕过拉黑回到好友列表,必须先拒掉;
            // getFriendState 在我侧 DISABLE 时直接返回 NONE,拿不到 BLOCKED 信号,这里显式补一次校验
            if (BooleanUtil.isTrue(peerFriend.getBlocked())) {
                throw exception(FRIEND_REQUEST_BLOCKED_BY_PEER);
            }
            friendService.silentReAddFriend(fromUserId, toUserId, reqVO.getDisplayName(), reqVO.getAddSource());
            return null;
        }
 
        // 2. 落库:同一申请人和接收人唯一,已有记录覆盖申请内容并重置为未处理
        ImFriendRequestDO request = createOrResetRequest(fromUserId, reqVO);
 
        // 3. 推送 FRIEND_REQUEST_RECEIVED 给 toUser 多端;payload 携带申请方昵称 / 头像,前端按 requestId 直推 push 进列表
        AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId);
        FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification()
                .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource())
                .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);
        if (fromUser != null) {
            payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar());
        }
        websocketService.sendNotificationAsync(toUserId, ImConversationTypeEnum.NONE.getType(),
                ImContentTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), payload);
 
        // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程
        //    回调内 try/catch 兜底 —— afterCommit 异常会被 Spring 静默吞掉,否则同意失败时申请方永远等不到 APPROVED
        if (imProperties.getFriend().isAutoAccept()) {
            Long requestId = request.getId();
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
 
                @Override
                public void afterCommit() {
                    try {
                        getSelf().agreeFriendRequest(toUserId, requestId);
                    } catch (Exception e) {
                        log.error("[applyFriend][autoAccept fromUserId={} toUserId={} requestId={} 自动通过失败]",
                                fromUserId, toUserId, requestId, e);
                    }
                }
 
            });
        }
        return request;
    }
 
    /**
     * 创建或重置好友申请
     *
     * @param fromUserId 申请人用户编号
     * @param reqVO      申请请求
     * @return 申请记录
     */
    private ImFriendRequestDO createOrResetRequest(Long fromUserId, ImFriendRequestApplyReqVO reqVO) {
        Long toUserId = reqVO.getToUserId();
        ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId);
        if (request == null) {
            // 1. 无旧申请:创建新申请;唯一键冲突时回查并复用并发写入的记录
            request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
                    .setFromUserId(fromUserId).setToUserId(toUserId)
                    .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult());
            try {
                friendRequestMapper.insert(request);
                return request;
            } catch (DuplicateKeyException ex) {
                request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId);
                if (request == null) {
                    throw ex;
                }
            }
        }
 
        // 2. 复用旧申请:覆盖本次申请内容,并重置为未处理
        LocalDateTime now = LocalDateTime.now();
        friendRequestMapper.updateByIdReset(request.getId(),
                reqVO.getApplyContent(), reqVO.getDisplayName(), reqVO.getAddSource(), now);
        // 同步内存对象,后续通知和自动通过直接复用
        request.setApplyContent(reqVO.getApplyContent()).setDisplayName(reqVO.getDisplayName())
                .setAddSource(reqVO.getAddSource())
                .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
                .setHandleContent(null).setHandleTime(null).setUpdateTime(now);
        return request;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void agreeFriendRequest(Long userId, Long requestId) {
        // 1.1 校验申请存在、未处理、操作人是接收方
        ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
 
        // 2. 乐观锁更新申请处理结果
        ImFriendRequestDO updateObj = new ImFriendRequestDO()
                .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(LocalDateTime.now());
        int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
                ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
        if (affected == 0) {
            throw exception(FRIEND_REQUEST_HANDLED);
        }
        request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(updateObj.getHandleTime());
 
        // 3. 双向建立好友关系
        friendService.becomeFriends(request);
 
        // 4. 推 FRIEND_REQUEST_APPROVED 给 fromUser 多端
        FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification)
                new FriendRequestApprovedNotification().setRequestId(request.getId())
                        .setOperatorUserId(userId).setFriendUserId(userId);
        websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
                ImContentTypeEnum.FRIEND_REQUEST_APPROVED.getType(), payload);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refuseFriendRequest(Long userId, Long requestId, String handleContent) {
        // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底)
        ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
 
        // 2. 乐观锁更新申请:handleResult=REFUSED + handleContent + handleTime;并发拒绝会有一方 affectedRows=0
        ImFriendRequestDO updateObj = new ImFriendRequestDO()
                .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult())
                .setHandleContent(handleContent).setHandleTime(LocalDateTime.now());
        int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),
                ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);
        if (affected == 0) {
            throw exception(FRIEND_REQUEST_HANDLED);
        }
 
        // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端
        FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification)
                new FriendRequestRejectedNotification().setRequestId(request.getId())
                        .setHandleContent(handleContent)
                        .setOperatorUserId(userId).setFriendUserId(userId);
        websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
                ImContentTypeEnum.FRIEND_REQUEST_REJECTED.getType(), payload);
    }
 
    @Override
    public List<ImFriendRequestDO> getMyFriendRequestList(Long userId, Long maxId, Integer limit) {
        ImFriendRequestDO maxRequest = maxId != null ? friendRequestMapper.selectById(maxId) : null;
        if (maxId != null && maxRequest == null) {
            return ListUtil.of();
        }
        return friendRequestMapper.selectMyList(userId,
                maxRequest != null ? maxRequest.getUpdateTime() : null,
                maxRequest != null ? maxRequest.getId() : null,
                limit);
    }
 
    @Override
    public List<ImFriendRequestDO> pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
        return friendRequestMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit);
    }
 
    @Override
    public ImFriendRequestDO getFriendRequest(Long id) {
        return friendRequestMapper.selectById(id);
    }
 
    @Override
    public PageResult<ImFriendRequestDO> getFriendRequestPage(ImFriendRequestManagerPageReqVO reqVO) {
        return friendRequestMapper.selectPage(reqVO);
    }
 
    /**
     * 校验申请可被「当前用户」处理:申请存在 + 未处理 + 操作人 = 接收方
     */
    private ImFriendRequestDO validateRequestForHandle(Long userId, Long requestId) {
        ImFriendRequestDO request = friendRequestMapper.selectById(requestId);
        if (request == null) {
            throw exception(FRIEND_REQUEST_NOT_EXISTS);
        }
        if (ObjUtil.notEqual(request.getToUserId(), userId)) {
            throw exception(FRIEND_REQUEST_NOT_TO_ME);
        }
        if (!ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) {
            throw exception(FRIEND_REQUEST_HANDLED);
        }
        return request;
    }
 
    private ImFriendRequestServiceImpl getSelf() {
        return SpringUtil.getBean(getClass());
    }
 
}