follow+sendMessage

Change-Id: I3e9bbcc89dfc53b9651fd8722da1b445a597629a
diff --git a/src/main/java/com/example/g8backend/controller/UserController.java b/src/main/java/com/example/g8backend/controller/UserController.java
index 2665b4c..3b9ec22 100644
--- a/src/main/java/com/example/g8backend/controller/UserController.java
+++ b/src/main/java/com/example/g8backend/controller/UserController.java
@@ -8,6 +8,8 @@
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Map;
+
 @RestController
 @RequestMapping("/user")
 public class UserController {
@@ -24,4 +26,21 @@
         user.setPassword(null);
         return ResponseEntity.ok(user);
     }
+    @PostMapping("/follow/{userId}")
+    public ResponseEntity<?> followUser(@PathVariable Long userId) {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        Long followerId = (Long) auth.getPrincipal(); // 确保Security返回Long
+        return ResponseEntity.ok(userService.followUser(followerId, userId));
+    }
+
+    @PostMapping("/message/{receiverId}")
+    public ResponseEntity<?> sendMessage(
+            @PathVariable Long receiverId,
+            @RequestBody String content
+    ) {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        Long senderId = (Long) auth.getPrincipal();
+        Long messageId = userService.sendMessage(senderId, receiverId, content);
+        return ResponseEntity.ok(Map.of("messageId", messageId));
+    }
 }
diff --git a/src/main/java/com/example/g8backend/entity/Follow.java b/src/main/java/com/example/g8backend/entity/Follow.java
new file mode 100644
index 0000000..f3ba6eb
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Follow.java
@@ -0,0 +1,16 @@
+// Follow.java(新增)
+package com.example.g8backend.entity;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user_follows")
+@Accessors(chain = true)
+public class Follow {
+    private Long followerId; // 保持Long类型
+    private Long followedId;
+    private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/Message.java b/src/main/java/com/example/g8backend/entity/Message.java
new file mode 100644
index 0000000..e16e244
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Message.java
@@ -0,0 +1,24 @@
+// Message.java(新增)
+package com.example.g8backend.entity;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+@TableName("private_messages")
+public class Message {
+    @TableId(type = IdType.AUTO)
+    private Long messageId;
+    private Long senderId;
+    private Long receiverId;
+    private String content;
+    private LocalDateTime sentAt;
+    @TableField("is_read")
+    private Boolean isRead = false; // ✅ 默认值
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/User.java b/src/main/java/com/example/g8backend/entity/User.java
index 7ba8e03..a9f5746 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -4,9 +4,11 @@
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
+import lombok.experimental.Accessors;
 
 @Data
 @TableName("users")
+@Accessors(chain = true)
 public class User {
     @TableId(type = IdType.AUTO)
     private Long userId;
@@ -24,4 +26,6 @@
                 ", email='" + email + '\'' +
                 '}';
     }
+
+
 }
diff --git a/src/main/java/com/example/g8backend/mapper/FollowMapper.java b/src/main/java/com/example/g8backend/mapper/FollowMapper.java
new file mode 100644
index 0000000..f1d03af
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/FollowMapper.java
@@ -0,0 +1,22 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Follow;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface FollowMapper extends BaseMapper<Follow> {
+    @Delete("DELETE FROM user_follows WHERE follower_id=#{followerId} AND followed_id=#{followedId}")
+    int deleteByPair(@Param("followerId") Long followerId, @Param("followedId") Long followedId);
+
+    @Select("SELECT followed_id FROM user_follows WHERE follower_id = #{userId}")
+    List<Long> selectFollowings(Long userId);
+
+    @Select("SELECT follower_id FROM user_follows WHERE followed_id = #{userId}")
+    List<Long> selectFollowers(Long userId);
+}
diff --git a/src/main/java/com/example/g8backend/mapper/MessageMapper.java b/src/main/java/com/example/g8backend/mapper/MessageMapper.java
new file mode 100644
index 0000000..3f2caf4
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/MessageMapper.java
@@ -0,0 +1,23 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Message;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface MessageMapper extends BaseMapper<Message> {
+    @Select("SELECT * FROM private_messages " +
+            "WHERE (sender_id=#{userId1} AND receiver_id=#{userId2}) " +
+            "OR (sender_id=#{userId2} AND receiver_id=#{userId1}) " +
+            "ORDER BY sent_at")
+    List<Message> selectConversation(@Param("userId1") Long userId1, @Param("userId2") Long userId2);
+
+    @Select("SELECT * FROM private_messages " +
+            "WHERE sender_id=#{userId} OR receiver_id=#{userId} " +
+            "ORDER BY sent_at DESC")
+    List<Message> selectUserMessages(Long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/service/IUserService.java b/src/main/java/com/example/g8backend/service/IUserService.java
index af94d27..1f178f8 100644
--- a/src/main/java/com/example/g8backend/service/IUserService.java
+++ b/src/main/java/com/example/g8backend/service/IUserService.java
@@ -1,11 +1,25 @@
 package com.example.g8backend.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.Message;
 import com.example.g8backend.entity.User;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 public interface IUserService extends IService<User> {
     User getUserByName(@Param("name") String name);
     User getUserByEmail(@Param("email") String email);
     User getUserByPasskey(@Param("passkey") String passkey);
+
+    // 关注功能
+    boolean followUser(Long followerId, Long followedId);
+    boolean unfollowUser(Long followerId, Long followedId);
+    List<User> getFollowings(Long userId);
+    List<User> getFollowers(Long userId);
+
+    // 私信功能
+    Long sendMessage(Long senderId, Long receiverId, String content);
+    List<Message> getMessages(Long userId, Long partnerId);
+    List<Message> getMessageHistory(Long userId);
 }
diff --git a/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
index e8bcf20..1d1b6dc 100644
--- a/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
@@ -1,12 +1,22 @@
 package com.example.g8backend.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Follow;
+import com.example.g8backend.entity.Message;
 import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.FollowMapper;
+import com.example.g8backend.mapper.MessageMapper;
 import com.example.g8backend.mapper.UserMapper;
 import com.example.g8backend.service.IUserService;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
 
 @Service
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@@ -22,4 +32,89 @@
     @Override
     public User getUserByPasskey(String passkey) { return userMapper.getUserByPasskey(passkey);}
 
+    @Resource
+    private FollowMapper followMapper;
+    @Resource
+    private MessageMapper messageMapper;
+
+    @Override
+    public boolean followUser(Long followerId, Long followedId) {
+        if (followerId.equals(followedId)) return false;
+        Follow follow = new Follow();
+        follow.setFollowerId(followerId);
+        follow.setFollowedId(followedId);
+        return followMapper.insert(follow) > 0;
+    }
+
+    @Override
+    public boolean unfollowUser(Long followerId, Long followedId) {
+        // 删除关注关系
+        return followMapper.deleteByPair(followerId, followedId) > 0;
+    }
+
+    @Override
+    public List<User> getFollowings(Long userId) {
+        // 1. 获取关注ID列表
+        List<Long> followingIds = followMapper.selectFollowings(userId);
+        // 2. 批量查询用户信息
+        return followingIds.isEmpty() ?
+                Collections.emptyList() :
+                this.listByIds(followingIds).stream()
+                        .peek(user -> user.setPassword(null)) // 敏感信息脱敏
+                        .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<User> getFollowers(Long userId) {
+        // 1. 获取粉丝ID列表
+        List<Long> followerIds = followMapper.selectFollowers(userId);
+        // 2. 批量查询用户信息
+        return followerIds.isEmpty() ?
+                Collections.emptyList() :
+                this.listByIds(followerIds).stream()
+                        .peek(user -> user.setPassword(null))
+                        .collect(Collectors.toList());
+    }
+
+    @Override
+    public Long sendMessage(Long senderId, Long receiverId, String content) {
+        // 1. 校验接收者是否存在(方案一)
+        if (userMapper.selectById(receiverId) == null) {
+            throw new RuntimeException("接收用户不存在");
+        }
+
+        // 2. 创建消息实体
+        Message message = new Message()
+                .setSenderId(senderId)
+                .setReceiverId(receiverId)
+                .setContent(content)
+                .setSentAt(LocalDateTime.now());
+
+        // 3. 插入数据库
+        messageMapper.insert(message);
+        return message.getMessageId();
+    }
+
+    @Override
+    public List<Message> getMessages(Long userId, Long partnerId) {
+        // 获取双方对话记录
+        return messageMapper.selectConversation(userId, partnerId).stream()
+                .peek(msg -> {
+                    // 标记消息为已读
+                    if (!msg.getIsRead() && msg.getReceiverId().equals(userId)) {
+                        msg.setIsRead(true);
+                        messageMapper.updateById(msg);
+                    }
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<Message> getMessageHistory(Long userId) {
+        // 获取用户所有相关消息
+        return messageMapper.selectUserMessages(userId).stream()
+                .sorted(Comparator.comparing(Message::getSentAt).reversed())
+                .collect(Collectors.toList());
+    }
+
 }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 6bde80e..7f85f87 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,4 +1,4 @@
-spring.datasource.password=123456
+spring.datasource.password=12345678
 spring.datasource.username=root
 spring.datasource.url=jdbc:mysql://localhost:3306/g8backend
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
diff --git a/src/main/resources/mapper/PostMapper.xml b/src/main/resources/mapper/PostMapper.xml
index c1dbda7..d1cbcf4 100644
--- a/src/main/resources/mapper/PostMapper.xml
+++ b/src/main/resources/mapper/PostMapper.xml
@@ -1,8 +1,8 @@
-.xml
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
+
 <mapper namespace="com.example.g8backend.mapper.PostMapper">
     <select id="getPostsByUserId" resultType="com.example.g8backend.entity.Post">
         SELECT * FROM posts WHERE user_id = #{userId}
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 952313e..5e5dd43 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -63,3 +63,25 @@
     FOREIGN KEY (post_id) REFERENCES posts(post_id),
     PRIMARY KEY (user_id, post_id)
 );
+
+-- 关注关系表
+CREATE TABLE IF NOT EXISTS `user_follows` (
+    follower_id INT NOT NULL,
+    followed_id INT NOT NULL,
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (follower_id) REFERENCES users(user_id),
+    FOREIGN KEY (followed_id) REFERENCES users(user_id),
+    PRIMARY KEY (follower_id, followed_id)
+);
+
+-- 私信表
+CREATE TABLE IF NOT EXISTS `private_messages` (
+    message_id INT AUTO_INCREMENT PRIMARY KEY,
+    sender_id INT NOT NULL,
+    receiver_id INT NOT NULL,
+    content TEXT NOT NULL,
+    sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    is_read BOOLEAN DEFAULT false,
+    FOREIGN KEY (sender_id) REFERENCES users(user_id),
+    FOREIGN KEY (receiver_id) REFERENCES users(user_id)
+);
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
index 5063501..6ddca58 100644
--- a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
+++ b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
@@ -4,6 +4,7 @@
 import com.example.g8backend.entity.Torrent;
 import com.example.g8backend.mapper.TorrentMapper;
 import com.example.g8backend.service.impl.TorrentServiceImpl;
+
 import com.example.g8backend.util.TorrentUtil;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/com/example/g8backend/service/UserServiceImplTest.java b/src/test/java/com/example/g8backend/service/UserServiceImplTest.java
new file mode 100644
index 0000000..96cf868
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/UserServiceImplTest.java
@@ -0,0 +1,149 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.example.g8backend.entity.Follow;
+import com.example.g8backend.entity.Message;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.FollowMapper;
+import com.example.g8backend.mapper.MessageMapper;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.impl.UserServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+public class UserServiceImplTest {
+
+    @Mock
+    private UserMapper userMapper;
+
+    @Mock
+    private FollowMapper followMapper;
+
+    @Mock
+    private MessageMapper messageMapper;
+
+    @InjectMocks
+    private UserServiceImpl userService;
+
+    private User user1;
+    private User user2;
+    private Follow follow;
+    private Message message;
+
+    @BeforeEach
+    void setUp() {
+        user1 = new User().setUserId(1L).setUserName("user1");
+        user2 = new User().setUserId(2L).setUserName("user2");
+        ReflectionTestUtils.setField(userService, "baseMapper", userMapper);
+
+        follow = new Follow().setFollowerId(1L).setFollowedId(2L);
+        message = new Message()
+                .setMessageId(100L)
+                .setSenderId(1L)
+                .setReceiverId(2L)
+                .setContent("Hello")
+                .setIsRead(false)
+                .setSentAt(LocalDateTime.now());
+    }
+
+    // ------------------------- 关注功能测试 -------------------------
+    @Test
+    void followUser_Success() {
+        // 模拟Mapper行为
+        when(followMapper.insert(any(Follow.class))).thenReturn(1);
+
+        // 测试关注
+        boolean result = userService.followUser(1L, 2L);
+        assertTrue(result);
+        verify(followMapper, times(1)).insert(any(Follow.class));
+    }
+
+    @Test
+    void followUser_SelfFollow_Fail() {
+        // 尝试关注自己
+        boolean result = userService.followUser(1L, 1L);
+        assertFalse(result);
+        verify(followMapper, never()).insert((Follow) any());
+    }
+
+    @Test
+    void unfollowUser_Success() {
+        // 模拟Mapper行为
+        when(followMapper.deleteByPair(1L, 2L)).thenReturn(1);
+
+        // 测试取消关注
+        boolean result = userService.unfollowUser(1L, 2L);
+        assertTrue(result);
+        verify(followMapper, times(1)).deleteByPair(1L, 2L);
+    }
+
+
+    @Test
+    void getFollowers_EmptyList() {
+        // 模拟无粉丝
+        when(followMapper.selectFollowers(1L)).thenReturn(Collections.emptyList());
+
+        // 测试获取粉丝列表
+        List<User> followers = userService.getFollowers(1L);
+        assertTrue(followers.isEmpty());
+    }
+
+    // ------------------------- 私信功能测试 -------------------------
+    @Test
+    void sendMessage_Success() {
+        // 模拟用户存在
+        when(userMapper.selectById(2L)).thenReturn(user2);
+        when(messageMapper.insert(any(Message.class))).thenAnswer(invocation -> {
+            Message msg = invocation.getArgument(0);
+            msg.setMessageId(100L);
+            return 1;
+        });
+
+        // 测试发送消息
+        Long messageId = userService.sendMessage(1L, 2L, "Hello");
+        assertNotNull(messageId);
+        verify(messageMapper, times(1)).insert(any(Message.class));
+    }
+
+    @Test
+    void sendMessage_ReceiverNotExist_ThrowException() {
+        // 模拟用户不存在
+        when(userMapper.selectById(99L)).thenReturn(null);
+
+        // 测试异常
+        assertThrows(RuntimeException.class, () ->
+                userService.sendMessage(1L, 99L, "Hello")
+        );
+        verify(messageMapper, never()).insert((Message) any());
+    }
+
+    @Test
+    void getMessageHistory_SortedByTime() {
+        // 模拟历史消息
+        Message oldMessage = message.setSentAt(LocalDateTime.now().minusHours(1));
+        when(messageMapper.selectUserMessages(1L))
+                .thenReturn(Arrays.asList(oldMessage, message));
+
+        // 测试按时间倒序排序
+        List<Message> history = userService.getMessageHistory(1L);
+        assertEquals(message.getMessageId(), history.get(0).getMessageId());
+    }
+}
\ No newline at end of file