diff --git a/im-service/src/main/java/com/xuqm/im/controller/ConversationController.java b/im-service/src/main/java/com/xuqm/im/controller/ConversationController.java new file mode 100644 index 0000000..4efba54 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/controller/ConversationController.java @@ -0,0 +1,33 @@ +package com.xuqm.im.controller; + +import com.xuqm.common.model.ApiResponse; +import com.xuqm.im.repository.ImMessageRepository; +import com.xuqm.im.service.MessageService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/im") +public class ConversationController { + + private final MessageService messageService; + + public ConversationController(MessageService messageService) { + this.messageService = messageService; + } + + @GetMapping("/conversations") + public ResponseEntity>> conversations( + @AuthenticationPrincipal String userId, + @RequestParam String appId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.success(messageService.conversations(appId, userId, size))); + } +} diff --git a/im-service/src/main/java/com/xuqm/im/controller/FriendController.java b/im-service/src/main/java/com/xuqm/im/controller/FriendController.java new file mode 100644 index 0000000..81f2c2b --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/controller/FriendController.java @@ -0,0 +1,78 @@ +package com.xuqm.im.controller; + +import com.xuqm.common.model.ApiResponse; +import com.xuqm.im.entity.ImFriendEntity; +import com.xuqm.im.repository.ImFriendRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/im/friends") +public class FriendController { + + private final ImFriendRepository friendRepository; + + public FriendController(ImFriendRepository friendRepository) { + this.friendRepository = friendRepository; + } + + @GetMapping + public ResponseEntity>> listFriends( + @AuthenticationPrincipal String userId, + @RequestParam String appId) { + List friendIds = friendRepository.findByAppIdAndUserId(appId, userId) + .stream() + .map(ImFriendEntity::getFriendId) + .toList(); + return ResponseEntity.ok(ApiResponse.success(friendIds)); + } + + @PostMapping + public ResponseEntity> addFriend( + @AuthenticationPrincipal String userId, + @RequestParam String appId, + @RequestParam String friendId) { + // Insert userId -> friendId if not already present + ImFriendEntity forward = friendRepository + .findByAppIdAndUserIdAndFriendId(appId, userId, friendId) + .orElseGet(() -> { + ImFriendEntity e = new ImFriendEntity(); + e.setAppId(appId); + e.setUserId(userId); + e.setFriendId(friendId); + return friendRepository.save(e); + }); + + // Insert friendId -> userId bi-directionally if not already present + friendRepository.findByAppIdAndUserIdAndFriendId(appId, friendId, userId) + .orElseGet(() -> { + ImFriendEntity e = new ImFriendEntity(); + e.setAppId(appId); + e.setUserId(friendId); + e.setFriendId(userId); + return friendRepository.save(e); + }); + + return ResponseEntity.ok(ApiResponse.success(forward)); + } + + @DeleteMapping("/{friendId}") + public ResponseEntity> removeFriend( + @AuthenticationPrincipal String userId, + @PathVariable String friendId, + @RequestParam String appId) { + // Remove both directions + friendRepository.deleteByAppIdAndUserIdAndFriendId(appId, userId, friendId); + friendRepository.deleteByAppIdAndUserIdAndFriendId(appId, friendId, userId); + return ResponseEntity.ok(ApiResponse.success(null)); + } +} diff --git a/im-service/src/main/java/com/xuqm/im/entity/ImFriendEntity.java b/im-service/src/main/java/com/xuqm/im/entity/ImFriendEntity.java new file mode 100644 index 0000000..8455e06 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/entity/ImFriendEntity.java @@ -0,0 +1,50 @@ +package com.xuqm.im.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.Instant; + +@Entity +@Table(name = "im_friends", + uniqueConstraints = @UniqueConstraint(columnNames = {"appId", "userId", "friendId"})) +public class ImFriendEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String appId; + + @Column(nullable = false, length = 128) + private String userId; + + @Column(nullable = false, length = 128) + private String friendId; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private Instant createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getAppId() { return appId; } + public void setAppId(String appId) { this.appId = appId; } + + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + + public String getFriendId() { return friendId; } + public void setFriendId(String friendId) { this.friendId = friendId; } + + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } +} diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImFriendRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImFriendRepository.java new file mode 100644 index 0000000..94044a0 --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/repository/ImFriendRepository.java @@ -0,0 +1,18 @@ +package com.xuqm.im.repository; + +import com.xuqm.im.entity.ImFriendEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +public interface ImFriendRepository extends JpaRepository { + + List findByAppIdAndUserId(String appId, String userId); + + Optional findByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId); + + @Transactional + void deleteByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId); +} diff --git a/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java b/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java index 6249636..7c9e096 100644 --- a/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java +++ b/im-service/src/main/java/com/xuqm/im/repository/ImMessageRepository.java @@ -7,8 +7,44 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.util.List; public interface ImMessageRepository extends JpaRepository { + + interface ConversationSummary { + String getTargetId(); + String getChatType(); + LocalDateTime getLastTime(); + } + + @Query(value = """ + SELECT target_id AS targetId, chat_type AS chatType, last_time AS lastTime + FROM ( + SELECT + CASE WHEN from_user_id = :userId THEN to_id ELSE from_user_id END AS target_id, + chat_type, + MAX(created_at) AS last_time + FROM im_message + WHERE app_id = :appId AND chat_type = 'SINGLE' + AND (from_user_id = :userId OR to_id = :userId) + GROUP BY target_id, chat_type + UNION ALL + SELECT to_id AS target_id, chat_type, MAX(created_at) AS last_time + FROM im_message + WHERE app_id = :appId AND chat_type = 'GROUP' + AND to_id IN ( + SELECT id FROM im_group + WHERE app_id = :appId AND FIND_IN_SET(:userId, member_ids) > 0 + ) + GROUP BY to_id, chat_type + ) combined + ORDER BY last_time DESC + LIMIT :size + """, nativeQuery = true) + List findConversations( + @Param("appId") String appId, + @Param("userId") String userId, + @Param("size") int size); Page findByAppIdAndToIdOrderByCreatedAtDesc( String appId, String toId, Pageable pageable); Page findByAppIdAndFromUserIdAndToIdOrderByCreatedAtDesc( diff --git a/im-service/src/main/java/com/xuqm/im/service/MessageService.java b/im-service/src/main/java/com/xuqm/im/service/MessageService.java index 9222254..9239273 100644 --- a/im-service/src/main/java/com/xuqm/im/service/MessageService.java +++ b/im-service/src/main/java/com/xuqm/im/service/MessageService.java @@ -6,7 +6,6 @@ import com.xuqm.im.cluster.ImClusterPublisher; import com.xuqm.im.entity.ImMessageEntity; import com.xuqm.im.entity.WebhookConfigEntity; import com.xuqm.im.model.SendMessageRequest; -import com.xuqm.im.repository.ImMessageRepository; import com.xuqm.im.repository.WebhookConfigRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -19,6 +18,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.LocalDateTime; +import com.xuqm.im.repository.ImMessageRepository; import java.util.List; import java.util.UUID; @@ -109,6 +109,10 @@ public class MessageService { appId, userId, toId, PageRequest.of(page, size)); } + public List conversations(String appId, String userId, int size) { + return messageRepository.findConversations(appId, userId, size); + } + @Async protected void dispatchWebhooks(String appId, ImMessageEntity message) { List webhooks = webhookRepository.findByAppIdAndEnabledTrue(appId);