diff --git a/README.md b/README.md index 51af298f8..28a7a843c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ OpenIsle 是一个基于 Spring Boot 的社区后端平台示例,提供注册 - **文章/评论**:支持发表文章并在文章下发布评论,评论可多级回复。 - **图片上传**:图片上传通过 `ImageUploader` 抽象实现,示例中提供基于腾讯云 COS 的 `CosImageUploader`。 - **用户头像**:`User` 模型新增 `avatar` 字段,可通过 `UserController` 上传并更新。 +- **用户信息**:`UserController` 允许获取任意用户资料,并列出最近的发帖和回复记录,可通过参数或配置调整条数。 ## 快速开始 diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java index dac951982..01c041e35 100644 --- a/src/main/java/com/openisle/controller/UserController.java +++ b/src/main/java/com/openisle/controller/UserController.java @@ -3,6 +3,8 @@ package com.openisle.controller; import com.openisle.model.User; import com.openisle.service.ImageUploader; import com.openisle.service.UserService; +import com.openisle.service.PostService; +import com.openisle.service.CommentService; import org.springframework.beans.factory.annotation.Value; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -20,6 +22,8 @@ import java.util.Map; public class UserController { private final UserService userService; private final ImageUploader imageUploader; + private final PostService postService; + private final CommentService commentService; @Value("${app.upload.check-type:true}") private boolean checkImageType; @@ -27,6 +31,12 @@ public class UserController { @Value("${app.upload.max-size:5242880}") private long maxUploadSize; + @Value("${app.user.posts-limit:10}") + private int defaultPostsLimit; + + @Value("${app.user.replies-limit:50}") + private int defaultRepliesLimit; + @GetMapping("/me") public ResponseEntity me(Authentication auth) { User user = userService.findByUsername(auth.getName()).orElseThrow(); @@ -52,6 +62,30 @@ public class UserController { return ResponseEntity.ok(Map.of("url", url)); } + @GetMapping("/{username}") + public ResponseEntity getUser(@PathVariable String username) { + User user = userService.findByUsername(username).orElseThrow(); + return ResponseEntity.ok(toDto(user)); + } + + @GetMapping("/{username}/posts") + public java.util.List userPosts(@PathVariable String username, + @RequestParam(value = "limit", required = false) Integer limit) { + int l = limit != null ? limit : defaultPostsLimit; + return postService.getRecentPostsByUser(username, l).stream() + .map(this::toMetaDto) + .collect(java.util.stream.Collectors.toList()); + } + + @GetMapping("/{username}/replies") + public java.util.List userReplies(@PathVariable String username, + @RequestParam(value = "limit", required = false) Integer limit) { + int l = limit != null ? limit : defaultRepliesLimit; + return commentService.getRecentCommentsByUser(username, l).stream() + .map(this::toCommentInfoDto) + .collect(java.util.stream.Collectors.toList()); + } + private UserDto toDto(User user) { UserDto dto = new UserDto(); dto.setId(user.getId()); @@ -61,6 +95,25 @@ public class UserController { return dto; } + private PostMetaDto toMetaDto(com.openisle.model.Post post) { + PostMetaDto dto = new PostMetaDto(); + dto.setId(post.getId()); + dto.setTitle(post.getTitle()); + dto.setCreatedAt(post.getCreatedAt()); + dto.setCategory(post.getCategory().getName()); + dto.setViews(post.getViews()); + return dto; + } + + private CommentInfoDto toCommentInfoDto(com.openisle.model.Comment comment) { + CommentInfoDto dto = new CommentInfoDto(); + dto.setId(comment.getId()); + dto.setContent(comment.getContent()); + dto.setCreatedAt(comment.getCreatedAt()); + dto.setPostId(comment.getPost().getId()); + return dto; + } + @Data private static class UserDto { private Long id; @@ -68,4 +121,21 @@ public class UserController { private String email; private String avatar; } + + @Data + private static class PostMetaDto { + private Long id; + private String title; + private java.time.LocalDateTime createdAt; + private String category; + private long views; + } + + @Data + private static class CommentInfoDto { + private Long id; + private String content; + private java.time.LocalDateTime createdAt; + private Long postId; + } } diff --git a/src/main/java/com/openisle/repository/CommentRepository.java b/src/main/java/com/openisle/repository/CommentRepository.java index ee7f3e23c..af028a4d5 100644 --- a/src/main/java/com/openisle/repository/CommentRepository.java +++ b/src/main/java/com/openisle/repository/CommentRepository.java @@ -2,11 +2,14 @@ package com.openisle.repository; import com.openisle.model.Comment; import com.openisle.model.Post; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import com.openisle.model.User; import java.util.List; public interface CommentRepository extends JpaRepository { List findByPostAndParentIsNullOrderByCreatedAtAsc(Post post); List findByParentOrderByCreatedAtAsc(Comment parent); + List findByAuthorOrderByCreatedAtDesc(User author, Pageable pageable); } diff --git a/src/main/java/com/openisle/repository/PostRepository.java b/src/main/java/com/openisle/repository/PostRepository.java index 63b3a9ab5..e328d7b72 100644 --- a/src/main/java/com/openisle/repository/PostRepository.java +++ b/src/main/java/com/openisle/repository/PostRepository.java @@ -2,10 +2,13 @@ package com.openisle.repository; import com.openisle.model.Post; import com.openisle.model.PostStatus; +import com.openisle.model.User; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface PostRepository extends JpaRepository { List findByStatus(PostStatus status); + List findByAuthorAndStatusOrderByCreatedAtDesc(User author, PostStatus status, Pageable pageable); } diff --git a/src/main/java/com/openisle/service/CommentService.java b/src/main/java/com/openisle/service/CommentService.java index 8b50c5d5a..e1906bc7c 100644 --- a/src/main/java/com/openisle/service/CommentService.java +++ b/src/main/java/com/openisle/service/CommentService.java @@ -10,6 +10,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; @Service @RequiredArgsConstructor @@ -54,4 +56,11 @@ public class CommentService { .orElseThrow(() -> new IllegalArgumentException("Comment not found")); return commentRepository.findByParentOrderByCreatedAtAsc(parent); } + + public List getRecentCommentsByUser(String username, int limit) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + Pageable pageable = PageRequest.of(0, limit); + return commentRepository.findByAuthorOrderByCreatedAtDesc(user, pageable); + } } diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java index d4fe356b3..15f1b3020 100644 --- a/src/main/java/com/openisle/service/PostService.java +++ b/src/main/java/com/openisle/service/PostService.java @@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; @Service public class PostService { @@ -59,6 +61,13 @@ public class PostService { return postRepository.findByStatus(PostStatus.PUBLISHED); } + public List getRecentPostsByUser(String username, int limit) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + Pageable pageable = PageRequest.of(0, limit); + return postRepository.findByAuthorAndStatusOrderByCreatedAtDesc(user, PostStatus.PUBLISHED, pageable); + } + public List listPendingPosts() { return postRepository.findByStatus(PostStatus.PENDING); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d192237b9..6a6e21197 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,3 +19,7 @@ app.jwt.expiration=${JWT_EXPIRATION:86400000} # Post publish mode: DIRECT or REVIEW app.post.publish-mode=${POST_PUBLISH_MODE:DIRECT} + +# Default list size for user posts and replies +app.user.posts-limit=${USER_POSTS_LIMIT:10} +app.user.replies-limit=${USER_REPLIES_LIMIT:50} diff --git a/src/test/java/com/openisle/controller/UserControllerTest.java b/src/test/java/com/openisle/controller/UserControllerTest.java index eec00f78c..f2297608f 100644 --- a/src/test/java/com/openisle/controller/UserControllerTest.java +++ b/src/test/java/com/openisle/controller/UserControllerTest.java @@ -3,6 +3,8 @@ package com.openisle.controller; import com.openisle.model.User; import com.openisle.service.ImageUploader; import com.openisle.service.UserService; +import com.openisle.service.PostService; +import com.openisle.service.CommentService; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +33,10 @@ class UserControllerTest { private UserService userService; @MockBean private ImageUploader imageUploader; + @MockBean + private PostService postService; + @MockBean + private CommentService commentService; @Test void getCurrentUser() throws Exception { @@ -69,4 +75,54 @@ class UserControllerTest { Mockito.verify(imageUploader, Mockito.never()).upload(any(), any()); } + @Test + void getUserByName() throws Exception { + User u = new User(); + u.setId(2L); + u.setUsername("bob"); + Mockito.when(userService.findByUsername("bob")).thenReturn(Optional.of(u)); + + mockMvc.perform(get("/api/users/bob")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(2)); + } + + @Test + void listUserPosts() throws Exception { + User user = new User(); + user.setUsername("bob"); + com.openisle.model.Category cat = new com.openisle.model.Category(); + cat.setName("tech"); + com.openisle.model.Post post = new com.openisle.model.Post(); + post.setId(3L); + post.setTitle("hello"); + post.setCreatedAt(java.time.LocalDateTime.now()); + post.setCategory(cat); + post.setAuthor(user); + Mockito.when(postService.getRecentPostsByUser("bob", 10)).thenReturn(java.util.List.of(post)); + + mockMvc.perform(get("/api/users/bob/posts")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].title").value("hello")); + } + + @Test + void listUserReplies() throws Exception { + User user = new User(); + user.setUsername("bob"); + com.openisle.model.Post post = new com.openisle.model.Post(); + post.setId(5L); + com.openisle.model.Comment comment = new com.openisle.model.Comment(); + comment.setId(4L); + comment.setContent("hi"); + comment.setCreatedAt(java.time.LocalDateTime.now()); + comment.setAuthor(user); + comment.setPost(post); + Mockito.when(commentService.getRecentCommentsByUser("bob", 50)).thenReturn(java.util.List.of(comment)); + + mockMvc.perform(get("/api/users/bob/replies")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(4)); + } + }