2026-04-27 23:41:58 +08:00
|
|
|
|
package com.xuqm.im.service;
|
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
import com.xuqm.common.exception.BusinessException;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
import com.xuqm.im.cluster.ImClusterPublisher;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
import com.xuqm.im.entity.ImFriendRequestEntity;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
import com.xuqm.im.entity.ImMessageEntity;
|
2026-04-29 12:33:25 +08:00
|
|
|
|
import com.xuqm.im.model.FriendRequestCallbackPayload;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
import com.xuqm.im.repository.ImFriendRequestRepository;
|
|
|
|
|
|
import com.xuqm.im.repository.ImFriendRepository;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
import com.xuqm.im.repository.ImMessageRepository;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
|
public class FriendRequestService {
|
|
|
|
|
|
|
|
|
|
|
|
private final ImFriendRequestRepository requestRepository;
|
|
|
|
|
|
private final ImFriendRepository friendRepository;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
private final ImMessageRepository messageRepository;
|
|
|
|
|
|
private final ImClusterPublisher clusterPublisher;
|
2026-04-29 12:33:25 +08:00
|
|
|
|
private final ImFeatureConfigClient featureConfigClient;
|
|
|
|
|
|
private final WebhookDispatchService webhookDispatchService;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
private final ObjectMapper objectMapper;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
|
|
|
|
|
|
public FriendRequestService(ImFriendRequestRepository requestRepository,
|
2026-04-28 16:08:07 +08:00
|
|
|
|
ImFriendRepository friendRepository,
|
|
|
|
|
|
ImMessageRepository messageRepository,
|
|
|
|
|
|
ImClusterPublisher clusterPublisher,
|
2026-04-29 12:33:25 +08:00
|
|
|
|
ImFeatureConfigClient featureConfigClient,
|
|
|
|
|
|
WebhookDispatchService webhookDispatchService,
|
2026-04-28 16:08:07 +08:00
|
|
|
|
ObjectMapper objectMapper) {
|
2026-04-27 23:41:58 +08:00
|
|
|
|
this.requestRepository = requestRepository;
|
|
|
|
|
|
this.friendRepository = friendRepository;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
this.messageRepository = messageRepository;
|
|
|
|
|
|
this.clusterPublisher = clusterPublisher;
|
2026-04-29 12:33:25 +08:00
|
|
|
|
this.featureConfigClient = featureConfigClient;
|
|
|
|
|
|
this.webhookDispatchService = webhookDispatchService;
|
2026-04-28 16:08:07 +08:00
|
|
|
|
this.objectMapper = objectMapper;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public ImFriendRequestEntity send(String appKey, String fromUserId, String toUserId, String remark) {
|
|
|
|
|
|
String mode = featureConfigClient.friendRequestMode(appKey);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
if ("DISALLOW".equals(mode)) {
|
|
|
|
|
|
throw new BusinessException(403, "当前应用未开放好友申请");
|
|
|
|
|
|
}
|
|
|
|
|
|
final boolean[] created = {false};
|
2026-05-07 19:39:42 +08:00
|
|
|
|
ImFriendRequestEntity saved = requestRepository.findByAppIdAndFromUserIdAndToUserId(appKey, fromUserId, toUserId)
|
2026-04-27 23:41:58 +08:00
|
|
|
|
.orElseGet(() -> {
|
2026-04-29 12:33:25 +08:00
|
|
|
|
created[0] = true;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
ImFriendRequestEntity entity = new ImFriendRequestEntity();
|
|
|
|
|
|
entity.setId(UUID.randomUUID().toString());
|
2026-05-07 19:39:42 +08:00
|
|
|
|
entity.setAppId(appKey);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
entity.setFromUserId(fromUserId);
|
|
|
|
|
|
entity.setToUserId(toUserId);
|
|
|
|
|
|
entity.setRemark(remark);
|
|
|
|
|
|
entity.setStatus(ImFriendRequestEntity.Status.PENDING.name());
|
|
|
|
|
|
entity.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
|
return requestRepository.save(entity);
|
|
|
|
|
|
});
|
2026-04-29 12:33:25 +08:00
|
|
|
|
if ("DIRECT_ACCEPT".equals(mode)) {
|
|
|
|
|
|
if (ImFriendRequestEntity.Status.ACCEPTED.name().equals(saved.getStatus())) {
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
return acceptRequest(saved);
|
|
|
|
|
|
}
|
2026-04-28 16:08:07 +08:00
|
|
|
|
if (!ImFriendRequestEntity.Status.PENDING.name().equals(saved.getStatus())) {
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
|
if (created[0]) {
|
|
|
|
|
|
dispatchWebhook(saved, "friend.request.sent");
|
|
|
|
|
|
}
|
2026-04-28 16:08:07 +08:00
|
|
|
|
publishNotification(
|
|
|
|
|
|
saved,
|
|
|
|
|
|
saved.getFromUserId(),
|
|
|
|
|
|
saved.getToUserId(),
|
|
|
|
|
|
"FRIEND_REQUEST",
|
|
|
|
|
|
"好友申请",
|
|
|
|
|
|
buildDescription("好友申请", saved.getRemark())
|
|
|
|
|
|
);
|
|
|
|
|
|
return saved;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public ImFriendRequestEntity accept(String appKey, String requestId, String operatorId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId, operatorId);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
request.setStatus(ImFriendRequestEntity.Status.ACCEPTED.name());
|
|
|
|
|
|
request.setReviewedAt(LocalDateTime.now());
|
|
|
|
|
|
requestRepository.save(request);
|
|
|
|
|
|
friendRepository
|
2026-05-07 19:39:42 +08:00
|
|
|
|
.findByAppIdAndUserIdAndFriendId(appKey, request.getFromUserId(), request.getToUserId())
|
|
|
|
|
|
.orElseGet(() -> friendEntity(appKey, request.getFromUserId(), request.getToUserId()));
|
2026-04-27 23:41:58 +08:00
|
|
|
|
friendRepository
|
2026-05-07 19:39:42 +08:00
|
|
|
|
.findByAppIdAndUserIdAndFriendId(appKey, request.getToUserId(), request.getFromUserId())
|
|
|
|
|
|
.orElseGet(() -> friendEntity(appKey, request.getToUserId(), request.getFromUserId()));
|
2026-04-29 12:33:25 +08:00
|
|
|
|
dispatchWebhook(request, "friend.request.accepted");
|
2026-04-28 16:08:07 +08:00
|
|
|
|
publishNotification(
|
|
|
|
|
|
request,
|
|
|
|
|
|
request.getToUserId(),
|
|
|
|
|
|
request.getFromUserId(),
|
|
|
|
|
|
"FRIEND_REQUEST_STATUS",
|
|
|
|
|
|
"好友申请已通过",
|
|
|
|
|
|
buildDescription("好友申请已通过", request.getRemark())
|
|
|
|
|
|
);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
return request;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public ImFriendRequestEntity reject(String appKey, String requestId, String operatorId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId, operatorId);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
request.setStatus(ImFriendRequestEntity.Status.REJECTED.name());
|
|
|
|
|
|
request.setReviewedAt(LocalDateTime.now());
|
2026-04-28 16:08:07 +08:00
|
|
|
|
ImFriendRequestEntity saved = requestRepository.save(request);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
dispatchWebhook(saved, "friend.request.rejected");
|
2026-04-28 16:08:07 +08:00
|
|
|
|
publishNotification(
|
|
|
|
|
|
saved,
|
|
|
|
|
|
saved.getToUserId(),
|
|
|
|
|
|
saved.getFromUserId(),
|
|
|
|
|
|
"FRIEND_REQUEST_STATUS",
|
|
|
|
|
|
"好友申请已拒绝",
|
|
|
|
|
|
buildDescription("好友申请已拒绝", saved.getRemark())
|
|
|
|
|
|
);
|
|
|
|
|
|
return saved;
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public List<ImFriendRequestEntity> acceptBatch(String appKey, List<String> requestIds, String operatorId) {
|
2026-04-28 21:05:06 +08:00
|
|
|
|
List<ImFriendRequestEntity> result = new java.util.ArrayList<>();
|
|
|
|
|
|
for (String requestId : unique(requestIds)) {
|
2026-05-07 19:39:42 +08:00
|
|
|
|
result.add(acceptInternal(appKey, requestId, operatorId));
|
2026-04-28 21:05:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public List<ImFriendRequestEntity> rejectBatch(String appKey, List<String> requestIds, String operatorId) {
|
2026-04-28 21:05:06 +08:00
|
|
|
|
List<ImFriendRequestEntity> result = new java.util.ArrayList<>();
|
|
|
|
|
|
for (String requestId : unique(requestIds)) {
|
2026-05-07 19:39:42 +08:00
|
|
|
|
result.add(rejectInternal(appKey, requestId, operatorId));
|
2026-04-28 21:05:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public List<ImFriendRequestEntity> incoming(String appKey, String userId) {
|
|
|
|
|
|
return requestRepository.findByAppIdAndToUserId(appKey, userId).stream()
|
2026-04-28 16:08:07 +08:00
|
|
|
|
.filter(request -> ImFriendRequestEntity.Status.PENDING.name().equals(request.getStatus()))
|
|
|
|
|
|
.toList();
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public List<ImFriendRequestEntity> outgoing(String appKey, String userId) {
|
|
|
|
|
|
return requestRepository.findByAppIdAndFromUserId(appKey, userId);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public List<ImFriendRequestEntity> listByApp(String appKey) {
|
|
|
|
|
|
return requestRepository.findByAppId(appKey);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public ImFriendRequestEntity adminAccept(String appKey, String requestId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
return acceptRequest(request);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Transactional
|
2026-05-07 19:39:42 +08:00
|
|
|
|
public ImFriendRequestEntity adminReject(String appKey, String requestId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
return rejectRequest(request);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
private ImFriendRequestEntity getRequest(String appKey, String requestId, String operatorId) {
|
2026-04-27 23:41:58 +08:00
|
|
|
|
ImFriendRequestEntity request = requestRepository.findById(requestId)
|
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "好友申请不存在"));
|
2026-05-07 19:39:42 +08:00
|
|
|
|
if (!request.getAppId().equals(appKey) || !request.getToUserId().equals(operatorId)) {
|
2026-04-27 23:41:58 +08:00
|
|
|
|
throw new BusinessException(403, "无权操作");
|
|
|
|
|
|
}
|
|
|
|
|
|
return request;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
private ImFriendRequestEntity getRequest(String appKey, String requestId) {
|
2026-04-29 12:33:25 +08:00
|
|
|
|
ImFriendRequestEntity request = requestRepository.findById(requestId)
|
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "好友申请不存在"));
|
2026-05-07 19:39:42 +08:00
|
|
|
|
if (!request.getAppId().equals(appKey)) {
|
2026-04-29 12:33:25 +08:00
|
|
|
|
throw new BusinessException(403, "无权操作");
|
|
|
|
|
|
}
|
|
|
|
|
|
return request;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
private ImFriendRequestEntity acceptInternal(String appKey, String requestId, String operatorId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId, operatorId);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
return acceptRequest(request);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ImFriendRequestEntity acceptRequest(ImFriendRequestEntity request) {
|
2026-04-28 21:05:06 +08:00
|
|
|
|
request.setStatus(ImFriendRequestEntity.Status.ACCEPTED.name());
|
|
|
|
|
|
request.setReviewedAt(LocalDateTime.now());
|
|
|
|
|
|
ImFriendRequestEntity saved = requestRepository.save(request);
|
|
|
|
|
|
friendRepository
|
2026-04-29 12:33:25 +08:00
|
|
|
|
.findByAppIdAndUserIdAndFriendId(request.getAppId(), request.getFromUserId(), request.getToUserId())
|
|
|
|
|
|
.orElseGet(() -> friendEntity(request.getAppId(), request.getFromUserId(), request.getToUserId()));
|
2026-04-28 21:05:06 +08:00
|
|
|
|
friendRepository
|
2026-04-29 12:33:25 +08:00
|
|
|
|
.findByAppIdAndUserIdAndFriendId(request.getAppId(), request.getToUserId(), request.getFromUserId())
|
|
|
|
|
|
.orElseGet(() -> friendEntity(request.getAppId(), request.getToUserId(), request.getFromUserId()));
|
|
|
|
|
|
dispatchWebhook(saved, "friend.request.accepted");
|
2026-04-28 21:05:06 +08:00
|
|
|
|
publishNotification(
|
|
|
|
|
|
request,
|
|
|
|
|
|
request.getToUserId(),
|
|
|
|
|
|
request.getFromUserId(),
|
|
|
|
|
|
"FRIEND_REQUEST_STATUS",
|
|
|
|
|
|
"好友申请已通过",
|
|
|
|
|
|
buildDescription("好友申请已通过", request.getRemark())
|
|
|
|
|
|
);
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
private ImFriendRequestEntity rejectInternal(String appKey, String requestId, String operatorId) {
|
|
|
|
|
|
ImFriendRequestEntity request = getRequest(appKey, requestId, operatorId);
|
2026-04-29 12:33:25 +08:00
|
|
|
|
return rejectRequest(request);
|
2026-04-28 21:05:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:39:42 +08:00
|
|
|
|
private com.xuqm.im.entity.ImFriendEntity friendEntity(String appKey, String userId, String friendId) {
|
2026-04-27 23:41:58 +08:00
|
|
|
|
com.xuqm.im.entity.ImFriendEntity entity = new com.xuqm.im.entity.ImFriendEntity();
|
2026-05-07 19:39:42 +08:00
|
|
|
|
entity.setAppId(appKey);
|
2026-04-27 23:41:58 +08:00
|
|
|
|
entity.setUserId(userId);
|
|
|
|
|
|
entity.setFriendId(friendId);
|
|
|
|
|
|
return friendRepository.save(entity);
|
|
|
|
|
|
}
|
2026-04-28 16:08:07 +08:00
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
|
private List<String> unique(List<String> requestIds) {
|
|
|
|
|
|
return requestIds == null ? List.of() : new java.util.ArrayList<>(new java.util.LinkedHashSet<>(requestIds));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 16:08:07 +08:00
|
|
|
|
private void publishNotification(
|
|
|
|
|
|
ImFriendRequestEntity request,
|
|
|
|
|
|
String fromUserId,
|
|
|
|
|
|
String toUserId,
|
|
|
|
|
|
String type,
|
|
|
|
|
|
String title,
|
|
|
|
|
|
String content
|
|
|
|
|
|
) {
|
|
|
|
|
|
ImMessageEntity message = new ImMessageEntity();
|
|
|
|
|
|
message.setId(UUID.randomUUID().toString());
|
|
|
|
|
|
message.setAppId(request.getAppId());
|
|
|
|
|
|
message.setFromUserId(fromUserId);
|
|
|
|
|
|
message.setToId(toUserId);
|
|
|
|
|
|
message.setChatType(ImMessageEntity.ChatType.SINGLE);
|
|
|
|
|
|
message.setMsgType(ImMessageEntity.MsgType.NOTIFY);
|
|
|
|
|
|
message.setContent(buildNotificationContent(type, title, content, request));
|
|
|
|
|
|
message.setStatus(ImMessageEntity.MsgStatus.SENT);
|
|
|
|
|
|
message.setCreatedAt(LocalDateTime.now());
|
|
|
|
|
|
ImMessageEntity saved = messageRepository.save(message);
|
|
|
|
|
|
clusterPublisher.publish("/user/" + toUserId + "/queue/messages", saved);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String buildNotificationContent(
|
|
|
|
|
|
String type,
|
|
|
|
|
|
String title,
|
|
|
|
|
|
String content,
|
|
|
|
|
|
ImFriendRequestEntity request
|
|
|
|
|
|
) {
|
|
|
|
|
|
ObjectNode node = objectMapper.createObjectNode();
|
|
|
|
|
|
node.put("type", type);
|
|
|
|
|
|
node.put("title", title);
|
|
|
|
|
|
node.put("content", content);
|
|
|
|
|
|
node.put("requestId", request.getId());
|
|
|
|
|
|
node.put("fromUserId", request.getFromUserId());
|
|
|
|
|
|
node.put("toUserId", request.getToUserId());
|
|
|
|
|
|
node.put("status", request.getStatus());
|
|
|
|
|
|
if (request.getRemark() != null) {
|
|
|
|
|
|
node.put("remark", request.getRemark());
|
|
|
|
|
|
}
|
|
|
|
|
|
return node.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String buildDescription(String prefix, String remark) {
|
|
|
|
|
|
if (remark == null || remark.isBlank()) {
|
|
|
|
|
|
return prefix;
|
|
|
|
|
|
}
|
|
|
|
|
|
return prefix + ":" + remark;
|
|
|
|
|
|
}
|
2026-04-29 12:33:25 +08:00
|
|
|
|
|
|
|
|
|
|
private ImFriendRequestEntity rejectRequest(ImFriendRequestEntity request) {
|
|
|
|
|
|
request.setStatus(ImFriendRequestEntity.Status.REJECTED.name());
|
|
|
|
|
|
request.setReviewedAt(LocalDateTime.now());
|
|
|
|
|
|
ImFriendRequestEntity saved = requestRepository.save(request);
|
|
|
|
|
|
dispatchWebhook(saved, "friend.request.rejected");
|
|
|
|
|
|
publishNotification(
|
|
|
|
|
|
saved,
|
|
|
|
|
|
saved.getToUserId(),
|
|
|
|
|
|
saved.getFromUserId(),
|
|
|
|
|
|
"FRIEND_REQUEST_STATUS",
|
|
|
|
|
|
"好友申请已拒绝",
|
|
|
|
|
|
buildDescription("好友申请已拒绝", saved.getRemark())
|
|
|
|
|
|
);
|
|
|
|
|
|
return saved;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void dispatchWebhook(ImFriendRequestEntity request, String callbackEvent) {
|
|
|
|
|
|
webhookDispatchService.dispatch(
|
|
|
|
|
|
request.getAppId(),
|
|
|
|
|
|
"friend_request",
|
|
|
|
|
|
callbackEvent,
|
|
|
|
|
|
new FriendRequestCallbackPayload(
|
|
|
|
|
|
request.getAppId(),
|
|
|
|
|
|
request.getId(),
|
|
|
|
|
|
request.getFromUserId(),
|
|
|
|
|
|
request.getToUserId(),
|
|
|
|
|
|
request.getRemark(),
|
|
|
|
|
|
request.getStatus(),
|
|
|
|
|
|
request.getReviewedAt() == null ? null : request.getReviewedAt().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-27 23:41:58 +08:00
|
|
|
|
}
|