package com.xuqm.im.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.xuqm.common.exception.BusinessException; import com.xuqm.im.cluster.ImClusterPublisher; import com.xuqm.im.entity.ImGroupEntity; import com.xuqm.im.entity.ImGroupJoinRequestEntity; import com.xuqm.im.entity.ImMessageEntity; import com.xuqm.im.entity.ImGroupMuteEntity; import com.xuqm.im.entity.ImAccountEntity; import com.xuqm.im.repository.ImGroupJoinRequestRepository; import com.xuqm.im.repository.ImAccountRepository; import com.xuqm.im.repository.ImGroupRepository; import com.xuqm.im.repository.ImGroupMuteRepository; import com.xuqm.im.repository.ImMessageRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.data.domain.PageRequest; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.UUID; @Service public class ImGroupService { private final ImGroupRepository groupRepository; private final ImGroupMuteRepository muteRepository; private final ImGroupJoinRequestRepository joinRequestRepository; private final ImMessageRepository messageRepository; private final ImAccountRepository accountRepository; private final ImClusterPublisher clusterPublisher; private final ObjectMapper objectMapper; public ImGroupService(ImGroupRepository groupRepository, ImGroupMuteRepository muteRepository, ImGroupJoinRequestRepository joinRequestRepository, ImMessageRepository messageRepository, ImAccountRepository accountRepository, ImClusterPublisher clusterPublisher, ObjectMapper objectMapper) { this.groupRepository = groupRepository; this.muteRepository = muteRepository; this.joinRequestRepository = joinRequestRepository; this.messageRepository = messageRepository; this.accountRepository = accountRepository; this.clusterPublisher = clusterPublisher; this.objectMapper = objectMapper; } @Transactional public ImGroupEntity create(String appId, String name, String creatorId, List memberIds, String groupType) { List members = new ArrayList<>(memberIds); if (!members.contains(creatorId)) members.add(creatorId); ImGroupEntity group = new ImGroupEntity(); group.setId(UUID.randomUUID().toString()); group.setAppId(appId); group.setName(name); group.setGroupType(normalizeGroupType(groupType)); group.setCreatorId(creatorId); group.setMemberIds(toJson(members)); group.setAdminIds(toJson(List.of(creatorId))); group.setAnnouncement(null); group.setCreatedAt(LocalDateTime.now()); return groupRepository.save(group); } public ImGroupEntity get(String groupId) { return groupRepository.findById(groupId) .orElseThrow(() -> new BusinessException(404, "群组不存在")); } public ImGroupEntity get(String groupId, String requesterId) { ImGroupEntity group = get(groupId); if (!memberIds(group).contains(requesterId) && !group.getCreatorId().equals(requesterId)) { throw new BusinessException(403, "不在群内"); } return group; } @Transactional public ImGroupEntity addMember(String groupId, String userId, String operatorId) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); List members = fromJson(group.getMemberIds()); if (!members.contains(userId)) { members.add(userId); group.setMemberIds(toJson(members)); groupRepository.save(group); } return group; } @Transactional public ImGroupEntity addMembers(String groupId, List userIds, String operatorId) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); List members = new ArrayList<>(fromJson(group.getMemberIds())); boolean changed = false; for (String userId : userIds == null ? List.of() : userIds) { if (userId == null || userId.isBlank()) { continue; } if (!members.contains(userId)) { members.add(userId); changed = true; } } if (changed) { group.setMemberIds(toJson(members)); return groupRepository.save(group); } return group; } @Transactional public ImGroupEntity removeMember(String groupId, String userId, String operatorId) { ImGroupEntity group = get(groupId); List admins = fromJson(group.getAdminIds()); if (!admins.contains(operatorId) && !group.getCreatorId().equals(operatorId)) { throw new BusinessException(403, "无权操作"); } List members = new ArrayList<>(fromJson(group.getMemberIds())); members.remove(userId); group.setMemberIds(toJson(members)); return groupRepository.save(group); } @Transactional public ImGroupEntity removeMembers(String groupId, List userIds, String operatorId) { ImGroupEntity group = get(groupId); List admins = fromJson(group.getAdminIds()); if (!admins.contains(operatorId) && !group.getCreatorId().equals(operatorId)) { throw new BusinessException(403, "无权操作"); } List members = new ArrayList<>(fromJson(group.getMemberIds())); boolean changed = false; for (String userId : userIds == null ? List.of() : userIds) { if (userId == null || userId.isBlank()) { continue; } changed |= members.remove(userId); } if (changed) { group.setMemberIds(toJson(members)); return groupRepository.save(group); } return group; } @Transactional public ImGroupEntity update(String groupId, String operatorId, String name, String announcement) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); if (name != null && !name.isBlank()) { group.setName(name); } if (announcement != null) { group.setAnnouncement(announcement); } return groupRepository.save(group); } @Transactional public ImGroupEntity setRole(String groupId, String operatorId, String userId, String role) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); List admins = new ArrayList<>(fromJson(group.getAdminIds())); if ("ADMIN".equalsIgnoreCase(role)) { if (!admins.contains(userId)) admins.add(userId); } else { admins.remove(userId); if (userId.equals(group.getCreatorId())) { throw new BusinessException(403, "群主不能降级"); } } group.setAdminIds(toJson(admins)); return groupRepository.save(group); } @Transactional public ImGroupEntity muteMember(String groupId, String operatorId, String userId, long minutes) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); ImGroupMuteEntity mute = muteRepository .findByGroupIdAndUserIdAndMutedUntilAfter(groupId, userId, LocalDateTime.now()) .orElseGet(() -> { ImGroupMuteEntity entity = new ImGroupMuteEntity(); entity.setId(UUID.randomUUID().toString()); entity.setGroupId(groupId); entity.setUserId(userId); entity.setCreatedAt(LocalDateTime.now()); return entity; }); mute.setMutedUntil(LocalDateTime.now().plusMinutes(Math.max(minutes, 0))); mute.setUpdatedAt(LocalDateTime.now()); muteRepository.save(mute); return group; } @Transactional public void dismiss(String groupId, String operatorId) { ImGroupEntity group = get(groupId); if (!group.getCreatorId().equals(operatorId)) { throw new BusinessException(403, "只有群主可以解散群"); } muteRepository.deleteByGroupId(groupId); groupRepository.delete(group); } @Transactional public void adminDismiss(String groupId) { muteRepository.deleteByGroupId(groupId); groupRepository.deleteById(groupId); } public boolean isMemberMuted(String groupId, String userId) { return muteRepository.findByGroupIdAndUserIdAndMutedUntilAfter(groupId, userId, LocalDateTime.now()).isPresent(); } public List memberIds(ImGroupEntity group) { return fromJson(group.getMemberIds()); } public List adminIds(ImGroupEntity group) { return fromJson(group.getAdminIds()); } public List listByApp(String appId) { return groupRepository.findByAppId(appId); } public List listUserGroups(String appId, String userId) { return groupRepository.findUserGroups(appId, userId); } public List listPublicGroups(String appId, String keyword) { String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(); return groupRepository.findByAppId(appId).stream() .filter(group -> "PUBLIC".equalsIgnoreCase(normalizeGroupType(group.getGroupType()))) .filter(group -> normalizedKeyword.isBlank() || group.getName().toLowerCase().contains(normalizedKeyword) || group.getId().toLowerCase().contains(normalizedKeyword)) .toList(); } public List searchGroups(String appId, String keyword, int size) { return groupRepository.searchByKeyword(appId, keyword, PageRequest.of(0, Math.max(size, 1))); } public List listMembers(String appId, String groupId, String requesterId) { ImGroupEntity group = get(groupId, requesterId); return resolveMembers(appId, memberIds(group)); } public List searchMembers(String appId, String groupId, String requesterId, String keyword, int size) { ImGroupEntity group = get(groupId, requesterId); List ids = memberIds(group); if (keyword == null || keyword.isBlank()) { return resolveMembers(appId, ids).stream().limit(Math.max(size, 1)).toList(); } LinkedHashSet memberIdSet = new LinkedHashSet<>(ids); return accountRepository.searchByKeyword(appId, keyword, PageRequest.of(0, Math.max(size, 1))) .stream() .filter(account -> memberIdSet.contains(account.getUserId())) .toList(); } @Transactional public ImGroupJoinRequestEntity sendJoinRequest(String appId, String groupId, String requesterId, String remark) { ImGroupEntity group = get(groupId); if (!group.getAppId().equals(appId)) { throw new BusinessException(403, "无权操作"); } if (!"PUBLIC".equalsIgnoreCase(normalizeGroupType(group.getGroupType()))) { throw new BusinessException(400, "该群不支持申请加入"); } if (memberIds(group).contains(requesterId)) { throw new BusinessException(400, "已经在群内"); } return joinRequestRepository.findByAppIdAndGroupIdAndRequesterId(appId, groupId, requesterId) .orElseGet(() -> { ImGroupJoinRequestEntity entity = new ImGroupJoinRequestEntity(); entity.setId(UUID.randomUUID().toString()); entity.setAppId(appId); entity.setGroupId(groupId); entity.setRequesterId(requesterId); entity.setRemark(remark); entity.setStatus(ImGroupJoinRequestEntity.Status.PENDING.name()); entity.setCreatedAt(LocalDateTime.now()); ImGroupJoinRequestEntity saved = joinRequestRepository.save(entity); publishJoinRequestNotification( group, requesterId, uniqueRecipients(group), "GROUP_JOIN_REQUEST", "入群申请", buildDescription("入群申请", remark), saved ); return saved; }); } public List listJoinRequests(String appId, String groupId, String operatorId) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); return joinRequestRepository.findByAppIdAndGroupId(appId, groupId); } @Transactional public ImGroupJoinRequestEntity acceptJoinRequest(String appId, String requestId, String operatorId) { ImGroupJoinRequestEntity request = getJoinRequest(appId, requestId); ImGroupEntity group = get(request.getGroupId()); ensureCanManage(group, operatorId); request.setStatus(ImGroupJoinRequestEntity.Status.ACCEPTED.name()); request.setReviewedAt(LocalDateTime.now()); ImGroupJoinRequestEntity saved = joinRequestRepository.save(request); addMemberInternal(group, request.getRequesterId()); publishJoinRequestNotification( group, operatorId, List.of(request.getRequesterId()), "GROUP_JOIN_REQUEST_STATUS", "入群申请已通过", buildDescription("入群申请已通过", null), saved ); return saved; } @Transactional public ImGroupJoinRequestEntity rejectJoinRequest(String appId, String requestId, String operatorId) { ImGroupJoinRequestEntity request = getJoinRequest(appId, requestId); ImGroupEntity group = get(request.getGroupId()); ensureCanManage(group, operatorId); request.setStatus(ImGroupJoinRequestEntity.Status.REJECTED.name()); request.setReviewedAt(LocalDateTime.now()); ImGroupJoinRequestEntity saved = joinRequestRepository.save(request); publishJoinRequestNotification( group, operatorId, List.of(request.getRequesterId()), "GROUP_JOIN_REQUEST_STATUS", "入群申请已拒绝", buildDescription("入群申请已拒绝", null), saved ); return saved; } @Transactional public List acceptJoinRequests(String appId, String groupId, List requestIds, String operatorId) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); List result = new ArrayList<>(); for (String requestId : unique(requestIds)) { result.add(acceptJoinRequestInternal(appId, group, requestId, operatorId)); } return result; } @Transactional public List rejectJoinRequests(String appId, String groupId, List requestIds, String operatorId) { ImGroupEntity group = get(groupId); ensureCanManage(group, operatorId); List result = new ArrayList<>(); for (String requestId : unique(requestIds)) { result.add(rejectJoinRequestInternal(appId, group, requestId, operatorId)); } return result; } private String toJson(List list) { try { return objectMapper.writeValueAsString(list); } catch (Exception e) { return "[]"; } } private List fromJson(String json) { try { return objectMapper.readValue(json, new TypeReference<>() {}); } catch (Exception e) { return new ArrayList<>(); } } private void ensureCanManage(ImGroupEntity group, String operatorId) { List admins = fromJson(group.getAdminIds()); if (!admins.contains(operatorId) && !group.getCreatorId().equals(operatorId)) { throw new BusinessException(403, "无权操作"); } } private List uniqueRecipients(ImGroupEntity group) { LinkedHashSet recipients = new LinkedHashSet<>(fromJson(group.getAdminIds())); recipients.add(group.getCreatorId()); return new ArrayList<>(recipients); } private void publishJoinRequestNotification( ImGroupEntity group, String fromUserId, List recipients, String type, String title, String content, ImGroupJoinRequestEntity request ) { for (String recipient : recipients) { if (recipient == null || recipient.isBlank() || recipient.equals(fromUserId)) continue; ImMessageEntity message = new ImMessageEntity(); message.setId(UUID.randomUUID().toString()); message.setAppId(group.getAppId()); message.setFromUserId(fromUserId); message.setToId(recipient); message.setChatType(ImMessageEntity.ChatType.SINGLE); message.setMsgType(ImMessageEntity.MsgType.NOTIFY); message.setContent(buildNotificationContent(type, title, content, request, group)); message.setStatus(ImMessageEntity.MsgStatus.SENT); message.setCreatedAt(LocalDateTime.now()); ImMessageEntity saved = messageRepository.save(message); clusterPublisher.publish("/user/" + recipient + "/queue/messages", saved); } } private String buildNotificationContent( String type, String title, String content, ImGroupJoinRequestEntity request, ImGroupEntity group ) { ObjectNode node = objectMapper.createObjectNode(); node.put("type", type); node.put("title", title); node.put("content", content); node.put("requestId", request.getId()); node.put("groupId", request.getGroupId()); node.put("groupName", group.getName()); node.put("requesterId", request.getRequesterId()); 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; } private void addMemberInternal(ImGroupEntity group, String userId) { List members = fromJson(group.getMemberIds()); if (!members.contains(userId)) { members.add(userId); group.setMemberIds(toJson(members)); groupRepository.save(group); } } private ImGroupJoinRequestEntity getJoinRequest(String appId, String requestId) { ImGroupJoinRequestEntity request = joinRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(404, "加群申请不存在")); if (!request.getAppId().equals(appId)) { throw new BusinessException(403, "无权操作"); } return request; } private ImGroupJoinRequestEntity acceptJoinRequestInternal(String appId, ImGroupEntity group, String requestId, String operatorId) { ImGroupJoinRequestEntity request = getJoinRequest(appId, requestId); if (!group.getId().equals(request.getGroupId())) { throw new BusinessException(400, "加群申请不属于当前群"); } ensureCanManage(group, operatorId); request.setStatus(ImGroupJoinRequestEntity.Status.ACCEPTED.name()); request.setReviewedAt(LocalDateTime.now()); ImGroupJoinRequestEntity saved = joinRequestRepository.save(request); addMemberInternal(group, request.getRequesterId()); publishJoinRequestNotification( group, operatorId, List.of(request.getRequesterId()), "GROUP_JOIN_REQUEST_STATUS", "入群申请已通过", buildDescription("入群申请已通过", null), saved ); return saved; } private ImGroupJoinRequestEntity rejectJoinRequestInternal(String appId, ImGroupEntity group, String requestId, String operatorId) { ImGroupJoinRequestEntity request = getJoinRequest(appId, requestId); if (!group.getId().equals(request.getGroupId())) { throw new BusinessException(400, "加群申请不属于当前群"); } ensureCanManage(group, operatorId); request.setStatus(ImGroupJoinRequestEntity.Status.REJECTED.name()); request.setReviewedAt(LocalDateTime.now()); ImGroupJoinRequestEntity saved = joinRequestRepository.save(request); publishJoinRequestNotification( group, operatorId, List.of(request.getRequesterId()), "GROUP_JOIN_REQUEST_STATUS", "入群申请已拒绝", buildDescription("入群申请已拒绝", null), saved ); return saved; } private String normalizeGroupType(String groupType) { return (groupType == null || groupType.isBlank()) ? "WORK" : groupType.trim().toUpperCase(); } private List unique(List values) { return values == null ? List.of() : new ArrayList<>(new LinkedHashSet<>(values)); } private List resolveMembers(String appId, List ids) { List members = new ArrayList<>(); for (String userId : ids == null ? List.of() : ids) { accountRepository.findByAppIdAndUserId(appId, userId).ifPresent(members::add); } return members; } }