diff --git a/backend/src/main/java/com/openisle/controller/ActivityController.java b/backend/src/main/java/com/openisle/controller/ActivityController.java index 998cd73eb..2ea603735 100644 --- a/backend/src/main/java/com/openisle/controller/ActivityController.java +++ b/backend/src/main/java/com/openisle/controller/ActivityController.java @@ -1,11 +1,12 @@ package com.openisle.controller; +import com.openisle.dto.MilkTeaInfoDto; +import com.openisle.dto.MilkTeaRedeemRequest; import com.openisle.model.Activity; import com.openisle.model.ActivityType; import com.openisle.model.User; import com.openisle.service.ActivityService; import com.openisle.service.UserService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -25,20 +26,20 @@ public class ActivityController { } @GetMapping("/milk-tea") - public MilkTeaInfo milkTea() { + public MilkTeaInfoDto milkTea() { Activity a = activityService.getByType(ActivityType.MILK_TEA); long count = activityService.countParticipants(a); if (!a.isEnded() && count >= 50) { activityService.end(a); } - MilkTeaInfo info = new MilkTeaInfo(); + MilkTeaInfoDto info = new MilkTeaInfoDto(); info.setRedeemCount(count); info.setEnded(a.isEnded()); return info; } @PostMapping("/milk-tea/redeem") - public java.util.Map redeemMilkTea(@RequestBody RedeemRequest req, Authentication auth) { + public java.util.Map redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) { User user = userService.findByIdentifier(auth.getName()).orElseThrow(); Activity a = activityService.getByType(ActivityType.MILK_TEA); boolean first = activityService.redeem(a, user, req.getContact()); @@ -47,15 +48,4 @@ public class ActivityController { } return java.util.Map.of("message", "updated"); } - - @Data - private static class MilkTeaInfo { - private long redeemCount; - private boolean ended; - } - - @Data - private static class RedeemRequest { - private String contact; - } } diff --git a/backend/src/main/java/com/openisle/controller/AdminConfigController.java b/backend/src/main/java/com/openisle/controller/AdminConfigController.java index 97fd4744d..cf3e7c7d6 100644 --- a/backend/src/main/java/com/openisle/controller/AdminConfigController.java +++ b/backend/src/main/java/com/openisle/controller/AdminConfigController.java @@ -1,13 +1,10 @@ package com.openisle.controller; -import com.openisle.model.PasswordStrength; -import com.openisle.model.PublishMode; +import com.openisle.dto.ConfigDto; +import com.openisle.service.AiUsageService; import com.openisle.service.PasswordValidator; import com.openisle.service.PostService; -import com.openisle.service.AiUsageService; import com.openisle.service.RegisterModeService; -import com.openisle.model.RegisterMode; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -47,11 +44,4 @@ public class AdminConfigController { return getConfig(); } - @Data - public static class ConfigDto { - private PublishMode publishMode; - private PasswordStrength passwordStrength; - private Integer aiFormatLimit; - private RegisterMode registerMode; - } } diff --git a/backend/src/main/java/com/openisle/controller/AdminPostController.java b/backend/src/main/java/com/openisle/controller/AdminPostController.java index 7bb6fffd7..8e17d1c3e 100644 --- a/backend/src/main/java/com/openisle/controller/AdminPostController.java +++ b/backend/src/main/java/com/openisle/controller/AdminPostController.java @@ -1,12 +1,11 @@ package com.openisle.controller; -import com.openisle.model.Post; +import com.openisle.dto.PostSummaryDto; +import com.openisle.mapper.PostMapper; import com.openisle.service.PostService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -18,77 +17,32 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class AdminPostController { private final PostService postService; + private final PostMapper postMapper; @GetMapping("/pending") - public List pendingPosts() { + public List pendingPosts() { return postService.listPendingPosts().stream() - .map(this::toDto) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } @PostMapping("/{id}/approve") - public PostDto approve(@PathVariable Long id) { - return toDto(postService.approvePost(id)); + public PostSummaryDto approve(@PathVariable Long id) { + return postMapper.toSummaryDto(postService.approvePost(id)); } @PostMapping("/{id}/reject") - public PostDto reject(@PathVariable Long id) { - return toDto(postService.rejectPost(id)); + public PostSummaryDto reject(@PathVariable Long id) { + return postMapper.toSummaryDto(postService.rejectPost(id)); } @PostMapping("/{id}/pin") - public PostDto pin(@PathVariable Long id) { - return toDto(postService.pinPost(id)); + public PostSummaryDto pin(@PathVariable Long id) { + return postMapper.toSummaryDto(postService.pinPost(id)); } @PostMapping("/{id}/unpin") - public PostDto unpin(@PathVariable Long id) { - return toDto(postService.unpinPost(id)); - } - - private PostDto toDto(Post post) { - PostDto dto = new PostDto(); - dto.setId(post.getId()); - dto.setTitle(post.getTitle()); - dto.setContent(post.getContent()); - dto.setCreatedAt(post.getCreatedAt()); - dto.setAuthor(post.getAuthor().getUsername()); - dto.setCategory(toCategoryDto(post.getCategory())); - dto.setViews(post.getViews()); - dto.setStatus(post.getStatus()); - dto.setPinnedAt(post.getPinnedAt()); - return dto; - } - - private CategoryDto toCategoryDto(com.openisle.model.Category c) { - CategoryDto dto = new CategoryDto(); - dto.setId(c.getId()); - dto.setName(c.getName()); - dto.setDescription(c.getDescription()); - dto.setIcon(c.getIcon()); - dto.setSmallIcon(c.getSmallIcon()); - return dto; - } - - @Data - private static class PostDto { - private Long id; - private String title; - private String content; - private LocalDateTime createdAt; - private String author; - private CategoryDto category; - private long views; - private com.openisle.model.PostStatus status; - private LocalDateTime pinnedAt; - } - - @Data - private static class CategoryDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; + public PostSummaryDto unpin(@PathVariable Long id) { + return postMapper.toSummaryDto(postService.unpinPost(id)); } } diff --git a/backend/src/main/java/com/openisle/controller/AdminTagController.java b/backend/src/main/java/com/openisle/controller/AdminTagController.java index 51ac5d2c6..3dba36f6c 100644 --- a/backend/src/main/java/com/openisle/controller/AdminTagController.java +++ b/backend/src/main/java/com/openisle/controller/AdminTagController.java @@ -1,9 +1,10 @@ package com.openisle.controller; +import com.openisle.dto.TagDto; +import com.openisle.mapper.TagMapper; import com.openisle.model.Tag; -import com.openisle.service.TagService; import com.openisle.service.PostService; -import lombok.Data; +import com.openisle.service.TagService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -16,11 +17,12 @@ import java.util.stream.Collectors; public class AdminTagController { private final TagService tagService; private final PostService postService; + private final TagMapper tagMapper; @GetMapping("/pending") public List pendingTags() { return tagService.listPendingTags().stream() - .map(t -> toDto(t, postService.countPostsByTag(t.getId()))) + .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .collect(Collectors.toList()); } @@ -28,27 +30,6 @@ public class AdminTagController { public TagDto approve(@PathVariable Long id) { Tag tag = tagService.approveTag(id); long count = postService.countPostsByTag(tag.getId()); - return toDto(tag, count); - } - - private TagDto toDto(Tag tag, long count) { - TagDto dto = new TagDto(); - dto.setId(tag.getId()); - dto.setName(tag.getName()); - dto.setDescription(tag.getDescription()); - dto.setIcon(tag.getIcon()); - dto.setSmallIcon(tag.getSmallIcon()); - dto.setCount(count); - return dto; - } - - @Data - private static class TagDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - private Long count; + return tagMapper.toDto(tag, count); } } diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index b27d8eeb5..903975855 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -1,24 +1,16 @@ package com.openisle.controller; -import com.openisle.model.User; -import com.openisle.service.EmailSender; -import com.openisle.service.JwtService; -import com.openisle.service.UserService; -import com.openisle.service.CaptchaService; -import com.openisle.service.GoogleAuthService; -import com.openisle.service.GithubAuthService; -import com.openisle.service.DiscordAuthService; -import com.openisle.service.TwitterAuthService; -import com.openisle.service.RegisterModeService; -import com.openisle.service.NotificationService; -import com.openisle.model.RegisterMode; -import com.openisle.repository.UserRepository; +import com.openisle.dto.*; import com.openisle.exception.FieldException; -import lombok.Data; +import com.openisle.model.RegisterMode; +import com.openisle.model.User; +import com.openisle.repository.UserRepository; +import com.openisle.service.*; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.beans.factory.annotation.Value; + import java.util.Map; import java.util.Optional; @@ -154,8 +146,8 @@ public class AuthController { )); } - if (req.getReason() == null || req.getReason().length() <= 20) { - return ResponseEntity.badRequest().body(Map.of( + if (req.getReason() == null || req.getReason().trim().length() <= 20) { + return ResponseEntity.badRequest().body(Map.of( "error", "Reason's length must longer than 20", "reason_code", "INVALID_CREDENTIALS" )); @@ -306,71 +298,5 @@ public class AuthController { } } - @Data - private static class RegisterRequest { - private String username; - private String email; - private String password; - private String captcha; - } - - @Data - private static class LoginRequest { - private String username; - private String password; - private String captcha; - } - - @Data - private static class GoogleLoginRequest { - private String idToken; - } - - @Data - private static class GithubLoginRequest { - private String code; - private String redirectUri; - } - - @Data - private static class DiscordLoginRequest { - private String code; - private String redirectUri; - } - - @Data - private static class TwitterLoginRequest { - private String code; - private String redirectUri; - private String codeVerifier; - } - - @Data - private static class VerifyRequest { - private String username; - private String code; - } - - @Data - private static class MakeReasonRequest { - private String token; - private String reason; - } - - @Data - private static class ForgotPasswordRequest { - private String email; - } - - @Data - private static class VerifyForgotRequest { - private String email; - private String code; - } - - @Data - private static class ResetPasswordRequest { - private String token; - private String password; - } + // DTO classes moved to com.openisle.dto package } diff --git a/backend/src/main/java/com/openisle/controller/CategoryController.java b/backend/src/main/java/com/openisle/controller/CategoryController.java index ab6efed0b..4df6f20af 100644 --- a/backend/src/main/java/com/openisle/controller/CategoryController.java +++ b/backend/src/main/java/com/openisle/controller/CategoryController.java @@ -1,9 +1,13 @@ package com.openisle.controller; +import com.openisle.dto.CategoryDto; +import com.openisle.dto.CategoryRequest; +import com.openisle.dto.PostSummaryDto; +import com.openisle.mapper.CategoryMapper; +import com.openisle.mapper.PostMapper; import com.openisle.model.Category; import com.openisle.service.CategoryService; import com.openisle.service.PostService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -16,19 +20,21 @@ import java.util.stream.Collectors; public class CategoryController { private final CategoryService categoryService; private final PostService postService; + private final PostMapper postMapper; + private final CategoryMapper categoryMapper; @PostMapping public CategoryDto create(@RequestBody CategoryRequest req) { Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByCategory(c.getId()); - return toDto(c, count); + return categoryMapper.toDto(c, count); } @PutMapping("/{id}") public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) { Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByCategory(c.getId()); - return toDto(c, count); + return categoryMapper.toDto(c, count); } @DeleteMapping("/{id}") @@ -39,7 +45,7 @@ public class CategoryController { @GetMapping public List list() { return categoryService.listCategories().stream() - .map(c -> toDto(c, postService.countPostsByCategory(c.getId()))) + .map(c -> categoryMapper.toDto(c, postService.countPostsByCategory(c.getId()))) .sorted((a, b) -> Long.compare(b.getCount(), a.getCount())) .collect(Collectors.toList()); } @@ -48,7 +54,7 @@ public class CategoryController { public CategoryDto get(@PathVariable Long id) { Category c = categoryService.getCategory(id); long count = postService.countPostsByCategory(c.getId()); - return toDto(c, count); + return categoryMapper.toDto(c, count); } @GetMapping("/{id}/posts") @@ -57,47 +63,7 @@ public class CategoryController { @RequestParam(value = "pageSize", required = false) Integer pageSize) { return postService.listPostsByCategories(java.util.List.of(id), page, pageSize) .stream() - .map(p -> { - PostSummaryDto dto = new PostSummaryDto(); - dto.setId(p.getId()); - dto.setTitle(p.getTitle()); - return dto; - }) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } - - private CategoryDto toDto(Category c, long count) { - CategoryDto dto = new CategoryDto(); - dto.setId(c.getId()); - dto.setName(c.getName()); - dto.setIcon(c.getIcon()); - dto.setSmallIcon(c.getSmallIcon()); - dto.setDescription(c.getDescription()); - dto.setCount(count); - return dto; - } - - @Data - private static class CategoryRequest { - private String name; - private String description; - private String icon; - private String smallIcon; - } - - @Data - private static class CategoryDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - private Long count; - } - - @Data - private static class PostSummaryDto { - private Long id; - private String title; - } } diff --git a/backend/src/main/java/com/openisle/controller/CommentController.java b/backend/src/main/java/com/openisle/controller/CommentController.java index 72e4c6571..e0837379c 100644 --- a/backend/src/main/java/com/openisle/controller/CommentController.java +++ b/backend/src/main/java/com/openisle/controller/CommentController.java @@ -6,19 +6,14 @@ import com.openisle.dto.CommentRequest; import com.openisle.mapper.CommentMapper; import com.openisle.service.CaptchaService; import com.openisle.service.CommentService; -import com.openisle.service.CaptchaService; import com.openisle.service.LevelService; -import com.openisle.service.ReactionService; -import com.openisle.model.CommentSort; -import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -30,7 +25,7 @@ public class CommentController { private final CommentService commentService; private final LevelService levelService; private final CaptchaService captchaService; - private final ReactionService reactionService; + private final CommentMapper commentMapper; @Value("${app.captcha.enabled:false}") private boolean captchaEnabled; @@ -48,7 +43,7 @@ public class CommentController { return ResponseEntity.badRequest().build(); } Comment comment = commentService.addComment(auth.getName(), postId, req.getContent()); - CommentDto dto = toDto(comment); + CommentDto dto = commentMapper.toDto(comment); dto.setReward(levelService.awardForComment(auth.getName())); log.debug("createComment succeeded for comment {}", comment.getId()); return ResponseEntity.ok(dto); @@ -64,7 +59,7 @@ public class CommentController { return ResponseEntity.badRequest().build(); } Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent()); - CommentDto dto = toDto(comment); + CommentDto dto = commentMapper.toDto(comment); dto.setReward(levelService.awardForComment(auth.getName())); log.debug("replyComment succeeded for comment {}", comment.getId()); return ResponseEntity.ok(dto); @@ -72,99 +67,19 @@ public class CommentController { @GetMapping("/posts/{postId}/comments") public List listComments(@PathVariable Long postId, - @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort) { + @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) { log.debug("listComments called for post {} with sort {}", postId, sort); List list = commentService.getCommentsForPost(postId, sort).stream() - .map(this::toDtoWithReplies) + .map(commentMapper::toDtoWithReplies) .collect(Collectors.toList()); log.debug("listComments returning {} comments", list.size()); return list; } - private CommentDto toDtoWithReplies(Comment comment) { - CommentDto dto = toDto(comment); - List replies = commentService.getReplies(comment.getId()).stream() - .map(this::toDtoWithReplies) - .collect(Collectors.toList()); - dto.setReplies(replies); - List reactions = reactionService.getReactionsForComment(comment.getId()).stream() - .map(this::toReactionDto) - .collect(Collectors.toList()); - dto.setReactions(reactions); - return dto; - } - @DeleteMapping("/comments/{id}") public void deleteComment(@PathVariable Long id, Authentication auth) { log.debug("deleteComment called by user {} for comment {}", auth.getName(), id); commentService.deleteComment(auth.getName(), id); log.debug("deleteComment completed for comment {}", id); } - - private CommentDto toDto(Comment comment) { - CommentDto dto = new CommentDto(); - dto.setId(comment.getId()); - dto.setContent(comment.getContent()); - dto.setCreatedAt(comment.getCreatedAt()); - dto.setAuthor(toAuthorDto(comment.getAuthor())); - dto.setReward(0); - return dto; - } - - private AuthorDto toAuthorDto(com.openisle.model.User user) { - AuthorDto dto = new AuthorDto(); - dto.setId(user.getId()); - dto.setUsername(user.getUsername()); - dto.setAvatar(user.getAvatar()); - return dto; - } - - @Data - private static class CommentRequest { - private String content; - private String captcha; - } - - @Data - private static class CommentDto { - private Long id; - private String content; - private LocalDateTime createdAt; - private AuthorDto author; - private List replies; - private List reactions; - private int reward; - } - - @Data - private static class AuthorDto { - private Long id; - private String username; - private String avatar; - } - - private ReactionDto toReactionDto(com.openisle.model.Reaction reaction) { - ReactionDto dto = new ReactionDto(); - dto.setId(reaction.getId()); - dto.setType(reaction.getType()); - dto.setUser(reaction.getUser().getUsername()); - if (reaction.getPost() != null) { - dto.setPostId(reaction.getPost().getId()); - } - if (reaction.getComment() != null) { - dto.setCommentId(reaction.getComment().getId()); - } - dto.setReward(0); - return dto; - } - - @Data - private static class ReactionDto { - private Long id; - private com.openisle.model.ReactionType type; - private String user; - private Long postId; - private Long commentId; - private int reward; - } } diff --git a/backend/src/main/java/com/openisle/controller/ConfigController.java b/backend/src/main/java/com/openisle/controller/ConfigController.java index a754877eb..b4c115978 100644 --- a/backend/src/main/java/com/openisle/controller/ConfigController.java +++ b/backend/src/main/java/com/openisle/controller/ConfigController.java @@ -1,9 +1,8 @@ package com.openisle.controller; -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; +import com.openisle.dto.SiteConfigDto; import com.openisle.service.RegisterModeService; -import com.openisle.model.RegisterMode; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -34,8 +33,8 @@ public class ConfigController { private final RegisterModeService registerModeService; @GetMapping("/config") - public ConfigResponse getConfig() { - ConfigResponse resp = new ConfigResponse(); + public SiteConfigDto getConfig() { + SiteConfigDto resp = new SiteConfigDto(); resp.setCaptchaEnabled(captchaEnabled); resp.setRegisterCaptchaEnabled(registerCaptchaEnabled); resp.setLoginCaptchaEnabled(loginCaptchaEnabled); @@ -45,15 +44,4 @@ public class ConfigController { resp.setRegisterMode(registerModeService.getRegisterMode()); return resp; } - - @Data - private static class ConfigResponse { - private boolean captchaEnabled; - private boolean registerCaptchaEnabled; - private boolean loginCaptchaEnabled; - private boolean postCaptchaEnabled; - private boolean commentCaptchaEnabled; - private int aiFormatLimit; - private RegisterMode registerMode; - } } diff --git a/backend/src/main/java/com/openisle/controller/DraftController.java b/backend/src/main/java/com/openisle/controller/DraftController.java index b47fa6ff2..4f9ecd408 100644 --- a/backend/src/main/java/com/openisle/controller/DraftController.java +++ b/backend/src/main/java/com/openisle/controller/DraftController.java @@ -1,32 +1,32 @@ package com.openisle.controller; +import com.openisle.dto.DraftDto; +import com.openisle.dto.DraftRequest; +import com.openisle.mapper.DraftMapper; import com.openisle.model.Draft; import com.openisle.service.DraftService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.util.List; -import java.util.stream.Collectors; - @RestController @RequestMapping("/api/drafts") @RequiredArgsConstructor public class DraftController { private final DraftService draftService; + private final DraftMapper draftMapper; @PostMapping public ResponseEntity saveDraft(@RequestBody DraftRequest req, Authentication auth) { Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); - return ResponseEntity.ok(toDto(draft)); + return ResponseEntity.ok(draftMapper.toDto(draft)); } @GetMapping("/me") public ResponseEntity getMyDraft(Authentication auth) { return draftService.getDraft(auth.getName()) - .map(d -> ResponseEntity.ok(toDto(d))) + .map(d -> ResponseEntity.ok(draftMapper.toDto(d))) .orElseGet(() -> ResponseEntity.noContent().build()); } @@ -35,33 +35,4 @@ public class DraftController { draftService.deleteDraft(auth.getName()); return ResponseEntity.ok().build(); } - - private DraftDto toDto(Draft draft) { - DraftDto dto = new DraftDto(); - dto.setId(draft.getId()); - dto.setTitle(draft.getTitle()); - dto.setContent(draft.getContent()); - if (draft.getCategory() != null) { - dto.setCategoryId(draft.getCategory().getId()); - } - dto.setTagIds(draft.getTags().stream().map(com.openisle.model.Tag::getId).collect(Collectors.toList())); - return dto; - } - - @Data - private static class DraftRequest { - private String title; - private String content; - private Long categoryId; - private List tagIds; - } - - @Data - private static class DraftDto { - private Long id; - private String title; - private String content; - private Long categoryId; - private List tagIds; - } } diff --git a/backend/src/main/java/com/openisle/controller/NotificationController.java b/backend/src/main/java/com/openisle/controller/NotificationController.java index 2e23fe452..5462b94fc 100644 --- a/backend/src/main/java/com/openisle/controller/NotificationController.java +++ b/backend/src/main/java/com/openisle/controller/NotificationController.java @@ -1,17 +1,14 @@ package com.openisle.controller; -import com.openisle.model.Notification; -import com.openisle.model.NotificationType; -import com.openisle.model.ReactionType; -import com.openisle.model.Comment; -import com.openisle.model.Post; +import com.openisle.dto.NotificationDto; +import com.openisle.dto.NotificationMarkReadRequest; +import com.openisle.dto.NotificationUnreadCountDto; +import com.openisle.mapper.NotificationMapper; import com.openisle.service.NotificationService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -21,122 +18,26 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class NotificationController { private final NotificationService notificationService; + private final NotificationMapper notificationMapper; @GetMapping public List list(@RequestParam(value = "read", required = false) Boolean read, Authentication auth) { return notificationService.listNotifications(auth.getName(), read).stream() - .map(this::toDto) + .map(notificationMapper::toDto) .collect(Collectors.toList()); } @GetMapping("/unread-count") - public UnreadCount unreadCount(Authentication auth) { + public NotificationUnreadCountDto unreadCount(Authentication auth) { long count = notificationService.countUnread(auth.getName()); - UnreadCount uc = new UnreadCount(); + NotificationUnreadCountDto uc = new NotificationUnreadCountDto(); uc.setCount(count); return uc; } @PostMapping("/read") - public void markRead(@RequestBody MarkReadRequest req, Authentication auth) { + public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) { notificationService.markRead(auth.getName(), req.getIds()); } - - private NotificationDto toDto(Notification n) { - NotificationDto dto = new NotificationDto(); - dto.setId(n.getId()); - dto.setType(n.getType()); - if (n.getPost() != null) { - dto.setPost(toPostDto(n.getPost())); - } - if (n.getComment() != null) { - dto.setComment(toCommentDto(n.getComment())); - Comment parent = n.getComment().getParent(); - if (parent != null) { - dto.setParentComment(toCommentDto(parent)); - } - } - if (n.getFromUser() != null) { - dto.setFromUser(toAuthorDto(n.getFromUser())); - } - if (n.getReactionType() != null) { - dto.setReactionType(n.getReactionType()); - } - dto.setApproved(n.getApproved()); - dto.setContent(n.getContent()); - dto.setRead(n.isRead()); - dto.setCreatedAt(n.getCreatedAt()); - return dto; - } - - private PostDto toPostDto(Post post) { - PostDto dto = new PostDto(); - dto.setId(post.getId()); - dto.setTitle(post.getTitle()); - return dto; - } - - private CommentDto toCommentDto(Comment comment) { - CommentDto dto = new CommentDto(); - dto.setId(comment.getId()); - dto.setContent(comment.getContent()); - dto.setCreatedAt(comment.getCreatedAt()); - dto.setAuthor(toAuthorDto(comment.getAuthor())); - return dto; - } - - private AuthorDto toAuthorDto(com.openisle.model.User user) { - AuthorDto dto = new AuthorDto(); - dto.setId(user.getId()); - dto.setUsername(user.getUsername()); - dto.setAvatar(user.getAvatar()); - return dto; - } - - @Data - private static class MarkReadRequest { - private List ids; - } - - @Data - private static class NotificationDto { - private Long id; - private NotificationType type; - private PostDto post; - private CommentDto comment; - private CommentDto parentComment; - private AuthorDto fromUser; - private ReactionType reactionType; - private String content; - private Boolean approved; - private boolean read; - private LocalDateTime createdAt; - } - - @Data - private static class PostDto { - private Long id; - private String title; - } - - @Data - private static class CommentDto { - private Long id; - private String content; - private LocalDateTime createdAt; - private AuthorDto author; - } - - @Data - private static class AuthorDto { - private Long id; - private String username; - private String avatar; - } - - @Data - private static class UnreadCount { - private long count; - } } diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 9b49ddc5c..8c1c16024 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -1,25 +1,22 @@ package com.openisle.controller; -import com.openisle.model.Comment; +import com.openisle.dto.PostDetailDto; +import com.openisle.dto.PostRequest; +import com.openisle.dto.PostSummaryDto; +import com.openisle.mapper.PostMapper; import com.openisle.model.Post; -import com.openisle.model.Reaction; -import com.openisle.service.CommentService; -import com.openisle.model.CommentSort; -import com.openisle.service.PostService; -import com.openisle.service.ReactionService; import com.openisle.service.CaptchaService; import com.openisle.service.DraftService; +import com.openisle.service.LevelService; +import com.openisle.service.PostService; import com.openisle.service.SubscriptionService; import com.openisle.service.UserVisitService; -import com.openisle.service.LevelService; -import lombok.Data; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import org.springframework.beans.factory.annotation.Value; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -28,13 +25,12 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class PostController { private final PostService postService; - private final CommentService commentService; - private final ReactionService reactionService; private final SubscriptionService subscriptionService; private final LevelService levelService; private final CaptchaService captchaService; private final DraftService draftService; private final UserVisitService userVisitService; + private final PostMapper postMapper; @Value("${app.captcha.enabled:false}") private boolean captchaEnabled; @@ -43,24 +39,24 @@ public class PostController { private boolean postCaptchaEnabled; @PostMapping - public ResponseEntity createPost(@RequestBody PostRequest req, Authentication auth) { + public ResponseEntity createPost(@RequestBody PostRequest req, Authentication auth) { if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { return ResponseEntity.badRequest().build(); } Post post = postService.createPost(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); draftService.deleteDraft(auth.getName()); - PostDto dto = toDto(post); + PostDetailDto dto = postMapper.toDetailDto(post, auth.getName()); dto.setReward(levelService.awardForPost(auth.getName())); return ResponseEntity.ok(dto); } @PutMapping("/{id}") - public ResponseEntity updatePost(@PathVariable Long id, @RequestBody PostRequest req, + public ResponseEntity updatePost(@PathVariable Long id, @RequestBody PostRequest req, Authentication auth) { Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); - return ResponseEntity.ok(toDto(post)); + return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName())); } @DeleteMapping("/{id}") @@ -69,14 +65,14 @@ public class PostController { } @GetMapping("/{id}") - public ResponseEntity getPost(@PathVariable Long id, Authentication auth) { + public ResponseEntity getPost(@PathVariable Long id, Authentication auth) { String viewer = auth != null ? auth.getName() : null; Post post = postService.viewPost(id, viewer); - return ResponseEntity.ok(toDto(post, viewer)); + return ResponseEntity.ok(postMapper.toDetailDto(post, viewer)); } @GetMapping - public List listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + public List listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagIds", required = false) List tagIds, @@ -101,19 +97,19 @@ public class PostController { if (hasCategories && hasTags) { return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize) - .stream().map(this::toDto).collect(Collectors.toList()); + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } if (hasTags) { return postService.listPostsByTags(tids, page, pageSize) - .stream().map(this::toDto).collect(Collectors.toList()); + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } return postService.listPostsByCategories(ids, page, pageSize) - .stream().map(this::toDto).collect(Collectors.toList()); + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } @GetMapping("/ranking") - public List rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + public List rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagIds", required = false) List tagIds, @@ -134,11 +130,11 @@ public class PostController { } return postService.listPostsByViews(ids, tids, page, pageSize) - .stream().map(this::toDto).collect(Collectors.toList()); + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } @GetMapping("/latest-reply") - public List latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + public List latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagIds", required = false) List tagIds, @@ -159,195 +155,6 @@ public class PostController { } return postService.listPostsByLatestReply(ids, tids, page, pageSize) - .stream().map(this::toDto).collect(Collectors.toList()); - } - - private PostDto toDto(Post post) { - PostDto dto = new PostDto(); - dto.setId(post.getId()); - dto.setTitle(post.getTitle()); - dto.setContent(post.getContent()); - dto.setCreatedAt(post.getCreatedAt()); - dto.setAuthor(toAuthorDto(post.getAuthor())); - dto.setCategory(toCategoryDto(post.getCategory())); - dto.setTags(post.getTags().stream().map(this::toTagDto).collect(Collectors.toList())); - dto.setViews(post.getViews()); - dto.setStatus(post.getStatus()); - dto.setPinnedAt(post.getPinnedAt()); - - List reactions = reactionService.getReactionsForPost(post.getId()) - .stream() - .map(this::toReactionDto) - .collect(Collectors.toList()); - dto.setReactions(reactions); - - List comments = commentService.getCommentsForPost(post.getId(), CommentSort.OLDEST) - .stream() - .map(this::toCommentDtoWithReplies) - .collect(Collectors.toList()); - dto.setComments(comments); - - java.util.List participants = commentService.getParticipants(post.getId(), 5); - dto.setParticipants(participants.stream().map(this::toAuthorDto).collect(Collectors.toList())); - - java.time.LocalDateTime last = commentService.getLastCommentTime(post.getId()); - dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); - dto.setReward(0); - - return dto; - } - - private PostDto toDto(Post post, String viewer) { - PostDto dto = toDto(post); - if (viewer != null) { - dto.setSubscribed(subscriptionService.isPostSubscribed(viewer, post.getId())); - } else { - dto.setSubscribed(false); - } - return dto; - } - - private CommentDto toCommentDtoWithReplies(Comment comment) { - CommentDto dto = toCommentDto(comment); - List replies = commentService.getReplies(comment.getId()).stream() - .map(this::toCommentDtoWithReplies) - .collect(Collectors.toList()); - dto.setReplies(replies); - - List reactions = reactionService.getReactionsForComment(comment.getId()) - .stream() - .map(this::toReactionDto) - .collect(Collectors.toList()); - dto.setReactions(reactions); - - return dto; - } - - private CommentDto toCommentDto(Comment comment) { - CommentDto dto = new CommentDto(); - dto.setId(comment.getId()); - dto.setContent(comment.getContent()); - dto.setCreatedAt(comment.getCreatedAt()); - dto.setAuthor(toAuthorDto(comment.getAuthor())); - dto.setReward(0); - return dto; - } - - private ReactionDto toReactionDto(Reaction reaction) { - ReactionDto dto = new ReactionDto(); - dto.setId(reaction.getId()); - dto.setType(reaction.getType()); - dto.setUser(reaction.getUser().getUsername()); - if (reaction.getPost() != null) { - dto.setPostId(reaction.getPost().getId()); - } - if (reaction.getComment() != null) { - dto.setCommentId(reaction.getComment().getId()); - } - dto.setReward(0); - return dto; - } - - private CategoryDto toCategoryDto(com.openisle.model.Category category) { - CategoryDto dto = new CategoryDto(); - dto.setId(category.getId()); - dto.setName(category.getName()); - dto.setDescription(category.getDescription()); - dto.setIcon(category.getIcon()); - dto.setSmallIcon(category.getSmallIcon()); - return dto; - } - - private TagDto toTagDto(com.openisle.model.Tag tag) { - TagDto dto = new TagDto(); - dto.setId(tag.getId()); - dto.setName(tag.getName()); - dto.setDescription(tag.getDescription()); - dto.setIcon(tag.getIcon()); - dto.setSmallIcon(tag.getSmallIcon()); - return dto; - } - - private AuthorDto toAuthorDto(com.openisle.model.User user) { - AuthorDto dto = new AuthorDto(); - dto.setId(user.getId()); - dto.setUsername(user.getUsername()); - dto.setAvatar(user.getAvatar()); - return dto; - } - - @Data - private static class PostRequest { - private Long categoryId; - private String title; - private String content; - private java.util.List tagIds; - private String captcha; - } - - @Data - private static class PostDto { - private Long id; - private String title; - private String content; - private LocalDateTime createdAt; - private AuthorDto author; - private CategoryDto category; - private java.util.List tags; - private long views; - private com.openisle.model.PostStatus status; - private LocalDateTime pinnedAt; - private LocalDateTime lastReplyAt; - private List comments; - private List reactions; - private java.util.List participants; - private boolean subscribed; - private int reward; - } - - @Data - private static class CategoryDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - } - - @Data - private static class TagDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - } - - @Data - private static class AuthorDto { - private Long id; - private String username; - private String avatar; - } - - @Data - private static class CommentDto { - private Long id; - private String content; - private LocalDateTime createdAt; - private AuthorDto author; - private List replies; - private List reactions; - private int reward; - } - - @Data - private static class ReactionDto { - private Long id; - private com.openisle.model.ReactionType type; - private String user; - private Long postId; - private Long commentId; - private int reward; + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } } diff --git a/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java b/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java index 800287b40..48450592a 100644 --- a/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java +++ b/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java @@ -1,7 +1,8 @@ package com.openisle.controller; +import com.openisle.dto.PushPublicKeyDto; +import com.openisle.dto.PushSubscriptionRequest; import com.openisle.service.PushSubscriptionService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -16,26 +17,14 @@ public class PushSubscriptionController { private String publicKey; @GetMapping("/public-key") - public PublicKeyResponse getPublicKey() { - PublicKeyResponse r = new PublicKeyResponse(); + public PushPublicKeyDto getPublicKey() { + PushPublicKeyDto r = new PushPublicKeyDto(); r.setKey(publicKey); return r; } @PostMapping("/subscribe") - public void subscribe(@RequestBody SubscriptionRequest req, Authentication auth) { + public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) { pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth()); } - - @Data - private static class PublicKeyResponse { - private String key; - } - - @Data - private static class SubscriptionRequest { - private String endpoint; - private String p256dh; - private String auth; - } } diff --git a/backend/src/main/java/com/openisle/controller/ReactionController.java b/backend/src/main/java/com/openisle/controller/ReactionController.java index 062fe313a..e5466f7b6 100644 --- a/backend/src/main/java/com/openisle/controller/ReactionController.java +++ b/backend/src/main/java/com/openisle/controller/ReactionController.java @@ -1,11 +1,13 @@ package com.openisle.controller; +import com.openisle.dto.ReactionDto; +import com.openisle.dto.ReactionRequest; +import com.openisle.mapper.ReactionMapper; import com.openisle.model.Reaction; import com.openisle.model.ReactionType; -import com.openisle.service.ReactionService; import com.openisle.service.LevelService; +import com.openisle.service.ReactionService; import jakarta.transaction.Transactional; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.*; public class ReactionController { private final ReactionService reactionService; private final LevelService levelService; + private final ReactionMapper reactionMapper; /** * Get all available reaction types. @@ -34,7 +37,7 @@ public class ReactionController { if (reaction == null) { return ResponseEntity.noContent().build(); } - ReactionDto dto = toDto(reaction); + ReactionDto dto = reactionMapper.toDto(reaction); dto.setReward(levelService.awardForReaction(auth.getName())); return ResponseEntity.ok(dto); } @@ -47,38 +50,8 @@ public class ReactionController { if (reaction == null) { return ResponseEntity.noContent().build(); } - ReactionDto dto = toDto(reaction); + ReactionDto dto = reactionMapper.toDto(reaction); dto.setReward(levelService.awardForReaction(auth.getName())); return ResponseEntity.ok(dto); } - - private ReactionDto toDto(Reaction reaction) { - ReactionDto dto = new ReactionDto(); - dto.setId(reaction.getId()); - dto.setType(reaction.getType()); - dto.setUser(reaction.getUser().getUsername()); - if (reaction.getPost() != null) { - dto.setPostId(reaction.getPost().getId()); - } - if (reaction.getComment() != null) { - dto.setCommentId(reaction.getComment().getId()); - } - dto.setReward(0); - return dto; - } - - @Data - private static class ReactionRequest { - private ReactionType type; - } - - @Data - private static class ReactionDto { - private Long id; - private ReactionType type; - private String user; - private Long postId; - private Long commentId; - private int reward; - } } diff --git a/backend/src/main/java/com/openisle/controller/SearchController.java b/backend/src/main/java/com/openisle/controller/SearchController.java index 067789d83..0380ee41b 100644 --- a/backend/src/main/java/com/openisle/controller/SearchController.java +++ b/backend/src/main/java/com/openisle/controller/SearchController.java @@ -1,10 +1,11 @@ package com.openisle.controller; -import com.openisle.model.Post; -import com.openisle.model.Comment; -import com.openisle.model.User; +import com.openisle.dto.PostSummaryDto; +import com.openisle.dto.SearchResultDto; +import com.openisle.dto.UserDto; +import com.openisle.mapper.PostMapper; +import com.openisle.mapper.UserMapper; import com.openisle.service.SearchService; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,32 +20,34 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class SearchController { private final SearchService searchService; + private final UserMapper userMapper; + private final PostMapper postMapper; @GetMapping("/users") public List searchUsers(@RequestParam String keyword) { return searchService.searchUsers(keyword).stream() - .map(this::toUserDto) + .map(userMapper::toDto) .collect(Collectors.toList()); } @GetMapping("/posts") - public List searchPosts(@RequestParam String keyword) { + public List searchPosts(@RequestParam String keyword) { return searchService.searchPosts(keyword).stream() - .map(this::toPostDto) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } @GetMapping("/posts/content") - public List searchPostsByContent(@RequestParam String keyword) { + public List searchPostsByContent(@RequestParam String keyword) { return searchService.searchPostsByContent(keyword).stream() - .map(this::toPostDto) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } @GetMapping("/posts/title") - public List searchPostsByTitle(@RequestParam String keyword) { + public List searchPostsByTitle(@RequestParam String keyword) { return searchService.searchPostsByTitle(keyword).stream() - .map(this::toPostDto) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } @@ -63,42 +66,4 @@ public class SearchController { }) .collect(Collectors.toList()); } - - private UserDto toUserDto(User user) { - UserDto dto = new UserDto(); - dto.setId(user.getId()); - dto.setUsername(user.getUsername()); - dto.setAvatar(user.getAvatar()); - return dto; - } - - private PostDto toPostDto(Post post) { - PostDto dto = new PostDto(); - dto.setId(post.getId()); - dto.setTitle(post.getTitle()); - return dto; - } - - @Data - private static class UserDto { - private Long id; - private String username; - private String avatar; - } - - @Data - private static class PostDto { - private Long id; - private String title; - } - - @Data - private static class SearchResultDto { - private String type; - private Long id; - private String text; - private String subText; - private String extra; - private Long postId; - } } diff --git a/backend/src/main/java/com/openisle/controller/TagController.java b/backend/src/main/java/com/openisle/controller/TagController.java index 450994acb..72db5eda5 100644 --- a/backend/src/main/java/com/openisle/controller/TagController.java +++ b/backend/src/main/java/com/openisle/controller/TagController.java @@ -1,12 +1,16 @@ package com.openisle.controller; -import com.openisle.model.Tag; -import com.openisle.service.TagService; -import com.openisle.service.PostService; -import com.openisle.repository.UserRepository; +import com.openisle.dto.PostSummaryDto; +import com.openisle.dto.TagDto; +import com.openisle.dto.TagRequest; +import com.openisle.mapper.PostMapper; +import com.openisle.mapper.TagMapper; import com.openisle.model.PublishMode; import com.openisle.model.Role; -import lombok.Data; +import com.openisle.model.Tag; +import com.openisle.repository.UserRepository; +import com.openisle.service.PostService; +import com.openisle.service.TagService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -20,6 +24,8 @@ public class TagController { private final TagService tagService; private final PostService postService; private final UserRepository userRepository; + private final PostMapper postMapper; + private final TagMapper tagMapper; @PostMapping public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) { @@ -38,14 +44,14 @@ public class TagController { approved, auth != null ? auth.getName() : null); long count = postService.countPostsByTag(tag.getId()); - return toDto(tag, count); + return tagMapper.toDto(tag, count); } @PutMapping("/{id}") public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) { Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByTag(tag.getId()); - return toDto(tag, count); + return tagMapper.toDto(tag, count); } @DeleteMapping("/{id}") @@ -57,7 +63,7 @@ public class TagController { public List list(@RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "limit", required = false) Integer limit) { List dtos = tagService.searchTags(keyword).stream() - .map(t -> toDto(t, postService.countPostsByTag(t.getId()))) + .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .sorted((a, b) -> Long.compare(b.getCount(), a.getCount())) .collect(Collectors.toList()); if (limit != null && limit > 0 && dtos.size() > limit) { @@ -70,7 +76,7 @@ public class TagController { public TagDto get(@PathVariable Long id) { Tag tag = tagService.getTag(id); long count = postService.countPostsByTag(tag.getId()); - return toDto(tag, count); + return tagMapper.toDto(tag, count); } @GetMapping("/{id}/posts") @@ -79,47 +85,7 @@ public class TagController { @RequestParam(value = "pageSize", required = false) Integer pageSize) { return postService.listPostsByTags(java.util.List.of(id), page, pageSize) .stream() - .map(p -> { - PostSummaryDto dto = new PostSummaryDto(); - dto.setId(p.getId()); - dto.setTitle(p.getTitle()); - return dto; - }) + .map(postMapper::toSummaryDto) .collect(Collectors.toList()); } - - private TagDto toDto(Tag tag, long count) { - TagDto dto = new TagDto(); - dto.setId(tag.getId()); - dto.setName(tag.getName()); - dto.setIcon(tag.getIcon()); - dto.setSmallIcon(tag.getSmallIcon()); - dto.setDescription(tag.getDescription()); - dto.setCount(count); - return dto; - } - - @Data - private static class TagRequest { - private String name; - private String description; - private String icon; - private String smallIcon; - } - - @Data - private static class TagDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - private Long count; - } - - @Data - private static class PostSummaryDto { - private Long id; - private String title; - } } diff --git a/backend/src/main/java/com/openisle/controller/UserController.java b/backend/src/main/java/com/openisle/controller/UserController.java index 0501c07b1..01afbabb6 100644 --- a/backend/src/main/java/com/openisle/controller/UserController.java +++ b/backend/src/main/java/com/openisle/controller/UserController.java @@ -1,11 +1,13 @@ package com.openisle.controller; +import com.openisle.dto.*; import com.openisle.exception.NotFoundException; +import com.openisle.mapper.TagMapper; +import com.openisle.mapper.UserMapper; import com.openisle.model.User; import com.openisle.service.*; -import org.springframework.beans.factory.annotation.Value; -import lombok.Data; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -25,10 +27,10 @@ public class UserController { private final ReactionService reactionService; private final TagService tagService; private final SubscriptionService subscriptionService; - private final PostReadService postReadService; - private final UserVisitService userVisitService; private final LevelService levelService; private final JwtService jwtService; + private final UserMapper userMapper; + private final TagMapper tagMapper; @Value("${app.upload.check-type:true}") private boolean checkImageType; @@ -45,13 +47,10 @@ public class UserController { @Value("${app.user.tags-limit:50}") private int defaultTagsLimit; - @Value("${app.snippet-length:50}") - private int snippetLength; - @GetMapping("/me") public ResponseEntity me(Authentication auth) { User user = userService.findByUsername(auth.getName()).orElseThrow(); - return ResponseEntity.ok(toDto(user, auth)); + return ResponseEntity.ok(userMapper.toDto(user, auth)); } @PostMapping("/me/avatar") @@ -79,7 +78,7 @@ public class UserController { User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(user.getUsername()), - "user", toDto(user, auth) + "user", userMapper.toDto(user, auth) )); } @@ -93,7 +92,7 @@ public class UserController { public ResponseEntity getUser(@PathVariable("identifier") String identifier, Authentication auth) { User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found")); - return ResponseEntity.ok(toDto(user, auth)); + return ResponseEntity.ok(userMapper.toDto(user, auth)); } @GetMapping("/{identifier}/posts") @@ -102,7 +101,7 @@ public class UserController { int l = limit != null ? limit : defaultPostsLimit; User user = userService.findByIdentifier(identifier).orElseThrow(); return postService.getRecentPostsByUser(user.getUsername(), l).stream() - .map(this::toMetaDto) + .map(userMapper::toMetaDto) .collect(java.util.stream.Collectors.toList()); } @@ -112,7 +111,7 @@ public class UserController { int l = limit != null ? limit : defaultRepliesLimit; User user = userService.findByIdentifier(identifier).orElseThrow(); return commentService.getRecentCommentsByUser(user.getUsername(), l).stream() - .map(this::toCommentInfoDto) + .map(userMapper::toCommentInfoDto) .collect(java.util.stream.Collectors.toList()); } @@ -123,7 +122,7 @@ public class UserController { User user = userService.findByIdentifier(identifier).orElseThrow(); java.util.List ids = reactionService.topPostIds(user.getUsername(), l); return postService.getPostsByIds(ids).stream() - .map(this::toMetaDto) + .map(userMapper::toMetaDto) .collect(java.util.stream.Collectors.toList()); } @@ -134,49 +133,29 @@ public class UserController { User user = userService.findByIdentifier(identifier).orElseThrow(); java.util.List ids = reactionService.topCommentIds(user.getUsername(), l); return commentService.getCommentsByIds(ids).stream() - .map(this::toCommentInfoDto) + .map(userMapper::toCommentInfoDto) .collect(java.util.stream.Collectors.toList()); } @GetMapping("/{identifier}/hot-tags") - public java.util.List hotTags(@PathVariable("identifier") String identifier, - @RequestParam(value = "limit", required = false) Integer limit) { + public java.util.List hotTags(@PathVariable("identifier") String identifier, + @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : 10; User user = userService.findByIdentifier(identifier).orElseThrow(); return tagService.getTagsByUser(user.getUsername()).stream() - .map(t -> { - TagInfoDto dto = new TagInfoDto(); - dto.setId(t.getId()); - dto.setName(t.getName()); - dto.setDescription(t.getDescription()); - dto.setIcon(t.getIcon()); - dto.setSmallIcon(t.getSmallIcon()); - dto.setCreatedAt(t.getCreatedAt()); - dto.setCount(postService.countPostsByTag(t.getId())); - return dto; - }) + .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .sorted((a, b) -> Long.compare(b.getCount(), a.getCount())) .limit(l) .collect(java.util.stream.Collectors.toList()); } @GetMapping("/{identifier}/tags") - public java.util.List userTags(@PathVariable("identifier") String identifier, - @RequestParam(value = "limit", required = false) Integer limit) { + public java.util.List userTags(@PathVariable("identifier") String identifier, + @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : defaultTagsLimit; User user = userService.findByIdentifier(identifier).orElseThrow(); return tagService.getRecentTagsByUser(user.getUsername(), l).stream() - .map(t -> { - TagInfoDto dto = new TagInfoDto(); - dto.setId(t.getId()); - dto.setName(t.getName()); - dto.setDescription(t.getDescription()); - dto.setIcon(t.getIcon()); - dto.setSmallIcon(t.getSmallIcon()); - dto.setCreatedAt(t.getCreatedAt()); - dto.setCount(postService.countPostsByTag(t.getId())); - return dto; - }) + .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .collect(java.util.stream.Collectors.toList()); } @@ -184,7 +163,7 @@ public class UserController { public java.util.List following(@PathVariable("identifier") String identifier) { User user = userService.findByIdentifier(identifier).orElseThrow(); return subscriptionService.getSubscribedUsers(user.getUsername()).stream() - .map(this::toDto) + .map(userMapper::toDto) .collect(java.util.stream.Collectors.toList()); } @@ -192,17 +171,14 @@ public class UserController { public java.util.List followers(@PathVariable("identifier") String identifier) { User user = userService.findByIdentifier(identifier).orElseThrow(); return subscriptionService.getSubscribers(user.getUsername()).stream() - .map(this::toDto) + .map(userMapper::toDto) .collect(java.util.stream.Collectors.toList()); } - /** - * List all administrator users. - */ @GetMapping("/admins") public java.util.List admins() { return userService.getAdmins().stream() - .map(this::toDto) + .map(userMapper::toDto) .collect(java.util.stream.Collectors.toList()); } @@ -215,157 +191,15 @@ public class UserController { int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit; int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit; java.util.List posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream() - .map(this::toMetaDto) + .map(userMapper::toMetaDto) .collect(java.util.stream.Collectors.toList()); java.util.List replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream() - .map(this::toCommentInfoDto) + .map(userMapper::toCommentInfoDto) .collect(java.util.stream.Collectors.toList()); UserAggregateDto dto = new UserAggregateDto(); - dto.setUser(toDto(user, auth)); + dto.setUser(userMapper.toDto(user, auth)); dto.setPosts(posts); dto.setReplies(replies); return ResponseEntity.ok(dto); } - - private UserDto toDto(User user, Authentication viewer) { - UserDto dto = new UserDto(); - dto.setId(user.getId()); - dto.setUsername(user.getUsername()); - dto.setEmail(user.getEmail()); - dto.setAvatar(user.getAvatar()); - dto.setRole(user.getRole().name()); - dto.setIntroduction(user.getIntroduction()); - dto.setFollowers(subscriptionService.countSubscribers(user.getUsername())); - dto.setFollowing(subscriptionService.countSubscribed(user.getUsername())); - dto.setCreatedAt(user.getCreatedAt()); - dto.setLastPostTime(postService.getLastPostTime(user.getUsername())); - dto.setLastCommentTime(commentService.getLastCommentTimeOfUserByUserId(user.getId())); - dto.setTotalViews(postService.getTotalViews(user.getUsername())); - dto.setVisitedDays(userVisitService.countVisits(user.getUsername())); - dto.setReadPosts(postReadService.countReads(user.getUsername())); - dto.setLikesSent(reactionService.countLikesSent(user.getUsername())); - dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername())); - dto.setExperience(user.getExperience()); - dto.setCurrentLevel(levelService.getLevel(user.getExperience())); - dto.setNextLevelExp(levelService.nextLevelExp(user.getExperience())); - if (viewer != null) { - dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername())); - } else { - dto.setSubscribed(false); - } - return dto; - } - - private UserDto toDto(User user) { - return toDto(user, null); - } - - private PostMetaDto toMetaDto(com.openisle.model.Post post) { - PostMetaDto dto = new PostMetaDto(); - dto.setId(post.getId()); - dto.setTitle(post.getTitle()); - String content = post.getContent(); - if (content == null) { - content = ""; - } - if (snippetLength >= 0) { - dto.setSnippet(content.length() > snippetLength ? content.substring(0, snippetLength) : content); - } else { - dto.setSnippet(content); - } - 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.setPost(toMetaDto(comment.getPost())); - if (comment.getParent() != null) { - ParentCommentDto pc = new ParentCommentDto(); - pc.setId(comment.getParent().getId()); - pc.setAuthor(comment.getParent().getAuthor().getUsername()); - pc.setContent(comment.getParent().getContent()); - dto.setParentComment(pc); - } - return dto; - } - - @Data - private static class UserDto { - private Long id; - private String username; - private String email; - private String avatar; - private String role; - private String introduction; - private long followers; - private long following; - private java.time.LocalDateTime createdAt; - private java.time.LocalDateTime lastPostTime; - private java.time.LocalDateTime lastCommentTime; - private long totalViews; - private long visitedDays; - private long readPosts; - private long likesSent; - private long likesReceived; - private boolean subscribed; - private int experience; - private int currentLevel; - private int nextLevelExp; - } - - @Data - private static class PostMetaDto { - private Long id; - private String title; - private String snippet; - 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 PostMetaDto post; - private ParentCommentDto parentComment; - } - - @Data - private static class TagInfoDto { - private Long id; - private String name; - private String description; - private String icon; - private String smallIcon; - private java.time.LocalDateTime createdAt; - private Long count; - } - - @Data - private static class ParentCommentDto { - private Long id; - private String author; - private String content; - } - - @Data - private static class UpdateProfileDto { - private String username; - private String introduction; - } - - @Data - private static class UserAggregateDto { - private UserDto user; - private java.util.List posts; - private java.util.List replies; - } } diff --git a/backend/src/main/java/com/openisle/dto/AuthorDto.java b/backend/src/main/java/com/openisle/dto/AuthorDto.java new file mode 100644 index 000000000..9892df611 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/AuthorDto.java @@ -0,0 +1,14 @@ +package com.openisle.dto; + +import lombok.Data; + +/** + * DTO representing a post or comment author. + */ +@Data +public class AuthorDto { + private Long id; + private String username; + private String avatar; +} + diff --git a/backend/src/main/java/com/openisle/dto/CategoryDto.java b/backend/src/main/java/com/openisle/dto/CategoryDto.java new file mode 100644 index 000000000..708fefa24 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CategoryDto.java @@ -0,0 +1,17 @@ +package com.openisle.dto; + +import lombok.Data; + +/** + * DTO representing a post category. + */ +@Data +public class CategoryDto { + private Long id; + private String name; + private String description; + private String icon; + private String smallIcon; + private Long count; +} + diff --git a/backend/src/main/java/com/openisle/dto/CategoryRequest.java b/backend/src/main/java/com/openisle/dto/CategoryRequest.java new file mode 100644 index 000000000..44deae3a7 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CategoryRequest.java @@ -0,0 +1,12 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request body for creating or updating a category. */ +@Data +public class CategoryRequest { + private String name; + private String description; + private String icon; + private String smallIcon; +} diff --git a/backend/src/main/java/com/openisle/dto/CommentDto.java b/backend/src/main/java/com/openisle/dto/CommentDto.java new file mode 100644 index 000000000..17babe624 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CommentDto.java @@ -0,0 +1,21 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * DTO representing a comment and its nested replies. + */ +@Data +public class CommentDto { + private Long id; + private String content; + private LocalDateTime createdAt; + private AuthorDto author; + private List replies; + private List reactions; + private int reward; +} + diff --git a/backend/src/main/java/com/openisle/dto/CommentInfoDto.java b/backend/src/main/java/com/openisle/dto/CommentInfoDto.java new file mode 100644 index 000000000..ccf31d04d --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CommentInfoDto.java @@ -0,0 +1,15 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** DTO for comment information in user profiles. */ +@Data +public class CommentInfoDto { + private Long id; + private String content; + private LocalDateTime createdAt; + private PostMetaDto post; + private ParentCommentDto parentComment; +} diff --git a/backend/src/main/java/com/openisle/dto/CommentRequest.java b/backend/src/main/java/com/openisle/dto/CommentRequest.java new file mode 100644 index 000000000..ca59b6f01 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/CommentRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request body for creating or replying to a comment. */ +@Data +public class CommentRequest { + private String content; + private String captcha; +} diff --git a/backend/src/main/java/com/openisle/dto/ConfigDto.java b/backend/src/main/java/com/openisle/dto/ConfigDto.java new file mode 100644 index 000000000..9745ee42f --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ConfigDto.java @@ -0,0 +1,15 @@ +package com.openisle.dto; + +import com.openisle.model.PasswordStrength; +import com.openisle.model.PublishMode; +import com.openisle.model.RegisterMode; +import lombok.Data; + +/** DTO for site configuration. */ +@Data +public class ConfigDto { + private PublishMode publishMode; + private PasswordStrength passwordStrength; + private Integer aiFormatLimit; + private RegisterMode registerMode; +} diff --git a/backend/src/main/java/com/openisle/dto/DiscordLoginRequest.java b/backend/src/main/java/com/openisle/dto/DiscordLoginRequest.java new file mode 100644 index 000000000..db424288c --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DiscordLoginRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request for Discord OAuth login. */ +@Data +public class DiscordLoginRequest { + private String code; + private String redirectUri; +} diff --git a/backend/src/main/java/com/openisle/dto/DraftDto.java b/backend/src/main/java/com/openisle/dto/DraftDto.java new file mode 100644 index 000000000..467fc1979 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DraftDto.java @@ -0,0 +1,15 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.util.List; + +/** DTO representing a saved draft. */ +@Data +public class DraftDto { + private Long id; + private String title; + private String content; + private Long categoryId; + private List tagIds; +} diff --git a/backend/src/main/java/com/openisle/dto/DraftRequest.java b/backend/src/main/java/com/openisle/dto/DraftRequest.java new file mode 100644 index 000000000..ba5b0dd43 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DraftRequest.java @@ -0,0 +1,14 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.util.List; + +/** Request body for saving a draft. */ +@Data +public class DraftRequest { + private String title; + private String content; + private Long categoryId; + private List tagIds; +} diff --git a/backend/src/main/java/com/openisle/dto/ForgotPasswordRequest.java b/backend/src/main/java/com/openisle/dto/ForgotPasswordRequest.java new file mode 100644 index 000000000..242e0285a --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ForgotPasswordRequest.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to trigger a forgot password email. */ +@Data +public class ForgotPasswordRequest { + private String email; +} diff --git a/backend/src/main/java/com/openisle/dto/GithubLoginRequest.java b/backend/src/main/java/com/openisle/dto/GithubLoginRequest.java new file mode 100644 index 000000000..ad2b55148 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/GithubLoginRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request for GitHub OAuth login. */ +@Data +public class GithubLoginRequest { + private String code; + private String redirectUri; +} diff --git a/backend/src/main/java/com/openisle/dto/GoogleLoginRequest.java b/backend/src/main/java/com/openisle/dto/GoogleLoginRequest.java new file mode 100644 index 000000000..b2ea269fe --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/GoogleLoginRequest.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request for Google OAuth login. */ +@Data +public class GoogleLoginRequest { + private String idToken; +} diff --git a/backend/src/main/java/com/openisle/dto/LoginRequest.java b/backend/src/main/java/com/openisle/dto/LoginRequest.java new file mode 100644 index 000000000..2ac1f4df7 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/LoginRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to login. */ +@Data +public class LoginRequest { + private String username; + private String password; + private String captcha; +} diff --git a/backend/src/main/java/com/openisle/dto/MakeReasonRequest.java b/backend/src/main/java/com/openisle/dto/MakeReasonRequest.java new file mode 100644 index 000000000..8c7c4e699 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/MakeReasonRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to submit a reason (e.g., for moderation). */ +@Data +public class MakeReasonRequest { + private String token; + private String reason; +} diff --git a/backend/src/main/java/com/openisle/dto/MilkTeaInfoDto.java b/backend/src/main/java/com/openisle/dto/MilkTeaInfoDto.java new file mode 100644 index 000000000..a7c7fb4f8 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/MilkTeaInfoDto.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Info about the milk tea activity. */ +@Data +public class MilkTeaInfoDto { + private long redeemCount; + private boolean ended; +} diff --git a/backend/src/main/java/com/openisle/dto/MilkTeaRedeemRequest.java b/backend/src/main/java/com/openisle/dto/MilkTeaRedeemRequest.java new file mode 100644 index 000000000..63cb17422 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/MilkTeaRedeemRequest.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to redeem the milk tea activity. */ +@Data +public class MilkTeaRedeemRequest { + private String contact; +} diff --git a/backend/src/main/java/com/openisle/dto/NotificationDto.java b/backend/src/main/java/com/openisle/dto/NotificationDto.java new file mode 100644 index 000000000..4b741c537 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/NotificationDto.java @@ -0,0 +1,23 @@ +package com.openisle.dto; + +import com.openisle.model.NotificationType; +import com.openisle.model.ReactionType; +import lombok.Data; + +import java.time.LocalDateTime; + +/** DTO representing a user notification. */ +@Data +public class NotificationDto { + private Long id; + private NotificationType type; + private PostSummaryDto post; + private CommentDto comment; + private CommentDto parentComment; + private AuthorDto fromUser; + private ReactionType reactionType; + private String content; + private Boolean approved; + private boolean read; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/openisle/dto/NotificationMarkReadRequest.java b/backend/src/main/java/com/openisle/dto/NotificationMarkReadRequest.java new file mode 100644 index 000000000..39b7d75e9 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/NotificationMarkReadRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.util.List; + +/** Request to mark notifications as read. */ +@Data +public class NotificationMarkReadRequest { + private List ids; +} diff --git a/backend/src/main/java/com/openisle/dto/NotificationUnreadCountDto.java b/backend/src/main/java/com/openisle/dto/NotificationUnreadCountDto.java new file mode 100644 index 000000000..37f16d005 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/NotificationUnreadCountDto.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import lombok.Data; + +/** DTO representing unread notification count. */ +@Data +public class NotificationUnreadCountDto { + private long count; +} diff --git a/backend/src/main/java/com/openisle/dto/ParentCommentDto.java b/backend/src/main/java/com/openisle/dto/ParentCommentDto.java new file mode 100644 index 000000000..f24029c60 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ParentCommentDto.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Data; + +/** DTO representing a parent comment. */ +@Data +public class ParentCommentDto { + private Long id; + private String author; + private String content; +} diff --git a/backend/src/main/java/com/openisle/dto/PostDetailDto.java b/backend/src/main/java/com/openisle/dto/PostDetailDto.java new file mode 100644 index 000000000..8cd8e402b --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PostDetailDto.java @@ -0,0 +1,16 @@ +package com.openisle.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * Detailed DTO for a post, including comments. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PostDetailDto extends PostSummaryDto { + private List comments; +} + diff --git a/backend/src/main/java/com/openisle/dto/PostMetaDto.java b/backend/src/main/java/com/openisle/dto/PostMetaDto.java new file mode 100644 index 000000000..a48aade20 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PostMetaDto.java @@ -0,0 +1,16 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** Lightweight post metadata used in user profile lists. */ +@Data +public class PostMetaDto { + private Long id; + private String title; + private String snippet; + private LocalDateTime createdAt; + private String category; + private long views; +} diff --git a/backend/src/main/java/com/openisle/dto/PostRequest.java b/backend/src/main/java/com/openisle/dto/PostRequest.java new file mode 100644 index 000000000..0c40537e1 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PostRequest.java @@ -0,0 +1,18 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.util.List; + +/** + * Request body for creating or updating a post. + */ +@Data +public class PostRequest { + private Long categoryId; + private String title; + private String content; + private List tagIds; + private String captcha; +} + diff --git a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java new file mode 100644 index 000000000..079dc8c2d --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java @@ -0,0 +1,30 @@ +package com.openisle.dto; + +import com.openisle.model.PostStatus; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Lightweight DTO for listing posts without comments. + */ +@Data +public class PostSummaryDto { + private Long id; + private String title; + private String content; + private LocalDateTime createdAt; + private AuthorDto author; + private CategoryDto category; + private List tags; + private long views; + private PostStatus status; + private LocalDateTime pinnedAt; + private LocalDateTime lastReplyAt; + private List reactions; + private List participants; + private boolean subscribed; + private int reward; +} + diff --git a/backend/src/main/java/com/openisle/dto/PushPublicKeyDto.java b/backend/src/main/java/com/openisle/dto/PushPublicKeyDto.java new file mode 100644 index 000000000..9aa6169c5 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PushPublicKeyDto.java @@ -0,0 +1,9 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Public key response for web push. */ +@Data +public class PushPublicKeyDto { + private String key; +} diff --git a/backend/src/main/java/com/openisle/dto/PushSubscriptionRequest.java b/backend/src/main/java/com/openisle/dto/PushSubscriptionRequest.java new file mode 100644 index 000000000..b62d204c3 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PushSubscriptionRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request body for saving a push subscription. */ +@Data +public class PushSubscriptionRequest { + private String endpoint; + private String p256dh; + private String auth; +} diff --git a/backend/src/main/java/com/openisle/dto/ReactionDto.java b/backend/src/main/java/com/openisle/dto/ReactionDto.java new file mode 100644 index 000000000..b57a3d333 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ReactionDto.java @@ -0,0 +1,18 @@ +package com.openisle.dto; + +import com.openisle.model.ReactionType; +import lombok.Data; + +/** + * DTO representing a reaction on a post or comment. + */ +@Data +public class ReactionDto { + private Long id; + private ReactionType type; + private String user; + private Long postId; + private Long commentId; + private int reward; +} + diff --git a/backend/src/main/java/com/openisle/dto/ReactionRequest.java b/backend/src/main/java/com/openisle/dto/ReactionRequest.java new file mode 100644 index 000000000..a2a69f3b4 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ReactionRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import com.openisle.model.ReactionType; +import lombok.Data; + +/** Request for reacting to a post or comment. */ +@Data +public class ReactionRequest { + private ReactionType type; +} diff --git a/backend/src/main/java/com/openisle/dto/RegisterRequest.java b/backend/src/main/java/com/openisle/dto/RegisterRequest.java new file mode 100644 index 000000000..4ac63c795 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/RegisterRequest.java @@ -0,0 +1,12 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to register a new user. */ +@Data +public class RegisterRequest { + private String username; + private String email; + private String password; + private String captcha; +} diff --git a/backend/src/main/java/com/openisle/dto/ResetPasswordRequest.java b/backend/src/main/java/com/openisle/dto/ResetPasswordRequest.java new file mode 100644 index 000000000..0043058a7 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/ResetPasswordRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to reset password. */ +@Data +public class ResetPasswordRequest { + private String token; + private String password; +} diff --git a/backend/src/main/java/com/openisle/dto/SearchResultDto.java b/backend/src/main/java/com/openisle/dto/SearchResultDto.java new file mode 100644 index 000000000..bae753533 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/SearchResultDto.java @@ -0,0 +1,14 @@ +package com.openisle.dto; + +import lombok.Data; + +/** DTO representing a search result entry. */ +@Data +public class SearchResultDto { + private String type; + private Long id; + private String text; + private String subText; + private String extra; + private Long postId; +} diff --git a/backend/src/main/java/com/openisle/dto/SiteConfigDto.java b/backend/src/main/java/com/openisle/dto/SiteConfigDto.java new file mode 100644 index 000000000..a2e9ced8d --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/SiteConfigDto.java @@ -0,0 +1,16 @@ +package com.openisle.dto; + +import com.openisle.model.RegisterMode; +import lombok.Data; + +/** Public site configuration values. */ +@Data +public class SiteConfigDto { + private boolean captchaEnabled; + private boolean registerCaptchaEnabled; + private boolean loginCaptchaEnabled; + private boolean postCaptchaEnabled; + private boolean commentCaptchaEnabled; + private int aiFormatLimit; + private RegisterMode registerMode; +} diff --git a/backend/src/main/java/com/openisle/dto/TagDto.java b/backend/src/main/java/com/openisle/dto/TagDto.java new file mode 100644 index 000000000..26cb31fb9 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/TagDto.java @@ -0,0 +1,20 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * DTO representing a tag. + */ +@Data +public class TagDto { + private Long id; + private String name; + private String description; + private String icon; + private String smallIcon; + private LocalDateTime createdAt; + private Long count; +} + diff --git a/backend/src/main/java/com/openisle/dto/TagRequest.java b/backend/src/main/java/com/openisle/dto/TagRequest.java new file mode 100644 index 000000000..4d0f32e0e --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/TagRequest.java @@ -0,0 +1,12 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request body for creating or updating a tag. */ +@Data +public class TagRequest { + private String name; + private String description; + private String icon; + private String smallIcon; +} diff --git a/backend/src/main/java/com/openisle/dto/TwitterLoginRequest.java b/backend/src/main/java/com/openisle/dto/TwitterLoginRequest.java new file mode 100644 index 000000000..e7e460907 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/TwitterLoginRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request for Twitter OAuth login. */ +@Data +public class TwitterLoginRequest { + private String code; + private String redirectUri; + private String codeVerifier; +} diff --git a/backend/src/main/java/com/openisle/dto/UpdateProfileDto.java b/backend/src/main/java/com/openisle/dto/UpdateProfileDto.java new file mode 100644 index 000000000..0dc34b326 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/UpdateProfileDto.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request body for updating user profile. */ +@Data +public class UpdateProfileDto { + private String username; + private String introduction; +} diff --git a/backend/src/main/java/com/openisle/dto/UserAggregateDto.java b/backend/src/main/java/com/openisle/dto/UserAggregateDto.java new file mode 100644 index 000000000..99fc7470a --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/UserAggregateDto.java @@ -0,0 +1,13 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.util.List; + +/** Aggregated user data including posts and replies. */ +@Data +public class UserAggregateDto { + private UserDto user; + private List posts; + private List replies; +} diff --git a/backend/src/main/java/com/openisle/dto/UserDto.java b/backend/src/main/java/com/openisle/dto/UserDto.java new file mode 100644 index 000000000..ec3ee8afd --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/UserDto.java @@ -0,0 +1,30 @@ +package com.openisle.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** Detailed user information. */ +@Data +public class UserDto { + private Long id; + private String username; + private String email; + private String avatar; + private String role; + private String introduction; + private long followers; + private long following; + private LocalDateTime createdAt; + private LocalDateTime lastPostTime; + private LocalDateTime lastCommentTime; + private long totalViews; + private long visitedDays; + private long readPosts; + private long likesSent; + private long likesReceived; + private boolean subscribed; + private int experience; + private int currentLevel; + private int nextLevelExp; +} diff --git a/backend/src/main/java/com/openisle/dto/VerifyForgotRequest.java b/backend/src/main/java/com/openisle/dto/VerifyForgotRequest.java new file mode 100644 index 000000000..b5b97a63c --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/VerifyForgotRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to verify a forgot password code. */ +@Data +public class VerifyForgotRequest { + private String email; + private String code; +} diff --git a/backend/src/main/java/com/openisle/dto/VerifyRequest.java b/backend/src/main/java/com/openisle/dto/VerifyRequest.java new file mode 100644 index 000000000..08deec734 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/VerifyRequest.java @@ -0,0 +1,10 @@ +package com.openisle.dto; + +import lombok.Data; + +/** Request to verify a user registration. */ +@Data +public class VerifyRequest { + private String username; + private String code; +} diff --git a/backend/src/main/java/com/openisle/mapper/CategoryMapper.java b/backend/src/main/java/com/openisle/mapper/CategoryMapper.java new file mode 100644 index 000000000..cd078b677 --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/CategoryMapper.java @@ -0,0 +1,25 @@ +package com.openisle.mapper; + +import com.openisle.dto.CategoryDto; +import com.openisle.model.Category; +import org.springframework.stereotype.Component; + +/** Mapper for category entities. */ +@Component +public class CategoryMapper { + + public CategoryDto toDto(Category c) { + return toDto(c, null); + } + + public CategoryDto toDto(Category c, Long count) { + CategoryDto dto = new CategoryDto(); + dto.setId(c.getId()); + dto.setName(c.getName()); + dto.setDescription(c.getDescription()); + dto.setIcon(c.getIcon()); + dto.setSmallIcon(c.getSmallIcon()); + dto.setCount(count); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/CommentMapper.java b/backend/src/main/java/com/openisle/mapper/CommentMapper.java new file mode 100644 index 000000000..d9b065b45 --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/CommentMapper.java @@ -0,0 +1,42 @@ +package com.openisle.mapper; + +import com.openisle.dto.CommentDto; +import com.openisle.model.Comment; +import com.openisle.service.CommentService; +import com.openisle.service.ReactionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; + +/** Mapper for comments including replies and reactions. */ +@Component +@RequiredArgsConstructor +public class CommentMapper { + + private final CommentService commentService; + private final ReactionService reactionService; + private final ReactionMapper reactionMapper; + private final UserMapper userMapper; + + public CommentDto toDto(Comment comment) { + CommentDto dto = new CommentDto(); + dto.setId(comment.getId()); + dto.setContent(comment.getContent()); + dto.setCreatedAt(comment.getCreatedAt()); + dto.setAuthor(userMapper.toAuthorDto(comment.getAuthor())); + dto.setReward(0); + return dto; + } + + public CommentDto toDtoWithReplies(Comment comment) { + CommentDto dto = toDto(comment); + dto.setReplies(commentService.getReplies(comment.getId()).stream() + .map(this::toDtoWithReplies) + .collect(Collectors.toList())); + dto.setReactions(reactionService.getReactionsForComment(comment.getId()).stream() + .map(reactionMapper::toDto) + .collect(Collectors.toList())); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/DraftMapper.java b/backend/src/main/java/com/openisle/mapper/DraftMapper.java new file mode 100644 index 000000000..5e93fb05d --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/DraftMapper.java @@ -0,0 +1,24 @@ +package com.openisle.mapper; + +import com.openisle.dto.DraftDto; +import com.openisle.model.Draft; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; + +/** Mapper for draft entities. */ +@Component +public class DraftMapper { + + public DraftDto toDto(Draft draft) { + DraftDto dto = new DraftDto(); + dto.setId(draft.getId()); + dto.setTitle(draft.getTitle()); + dto.setContent(draft.getContent()); + if (draft.getCategory() != null) { + dto.setCategoryId(draft.getCategory().getId()); + } + dto.setTagIds(draft.getTags().stream().map(tag -> tag.getId()).collect(Collectors.toList())); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/NotificationMapper.java b/backend/src/main/java/com/openisle/mapper/NotificationMapper.java new file mode 100644 index 000000000..26bc751b3 --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/NotificationMapper.java @@ -0,0 +1,47 @@ +package com.openisle.mapper; + +import com.openisle.dto.NotificationDto; +import com.openisle.dto.PostSummaryDto; +import com.openisle.model.Comment; +import com.openisle.model.Notification; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** Mapper for notifications. */ +@Component +@RequiredArgsConstructor +public class NotificationMapper { + + private final CommentMapper commentMapper; + private final UserMapper userMapper; + + public NotificationDto toDto(Notification n) { + NotificationDto dto = new NotificationDto(); + dto.setId(n.getId()); + dto.setType(n.getType()); + if (n.getPost() != null) { + PostSummaryDto postDto = new PostSummaryDto(); + postDto.setId(n.getPost().getId()); + postDto.setTitle(n.getPost().getTitle()); + dto.setPost(postDto); + } + if (n.getComment() != null) { + dto.setComment(commentMapper.toDto(n.getComment())); + Comment parent = n.getComment().getParent(); + if (parent != null) { + dto.setParentComment(commentMapper.toDto(parent)); + } + } + if (n.getFromUser() != null) { + dto.setFromUser(userMapper.toAuthorDto(n.getFromUser())); + } + if (n.getReactionType() != null) { + dto.setReactionType(n.getReactionType()); + } + dto.setApproved(n.getApproved()); + dto.setContent(n.getContent()); + dto.setRead(n.isRead()); + dto.setCreatedAt(n.getCreatedAt()); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java new file mode 100644 index 000000000..8b5d36e0c --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -0,0 +1,78 @@ +package com.openisle.mapper; + +import com.openisle.dto.CommentDto; +import com.openisle.dto.PostDetailDto; +import com.openisle.dto.PostSummaryDto; +import com.openisle.dto.ReactionDto; +import com.openisle.model.CommentSort; +import com.openisle.model.Post; +import com.openisle.model.User; +import com.openisle.service.CommentService; +import com.openisle.service.ReactionService; +import com.openisle.service.SubscriptionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** Mapper responsible for converting posts into DTOs. */ +@Component +@RequiredArgsConstructor +public class PostMapper { + + private final CommentService commentService; + private final ReactionService reactionService; + private final SubscriptionService subscriptionService; + private final CommentMapper commentMapper; + private final ReactionMapper reactionMapper; + private final UserMapper userMapper; + private final TagMapper tagMapper; + private final CategoryMapper categoryMapper; + + public PostSummaryDto toSummaryDto(Post post) { + PostSummaryDto dto = new PostSummaryDto(); + applyCommon(post, dto); + return dto; + } + + public PostDetailDto toDetailDto(Post post, String viewer) { + PostDetailDto dto = new PostDetailDto(); + applyCommon(post, dto); + List comments = commentService.getCommentsForPost(post.getId(), CommentSort.OLDEST) + .stream() + .map(commentMapper::toDtoWithReplies) + .collect(Collectors.toList()); + dto.setComments(comments); + dto.setSubscribed(viewer != null && subscriptionService.isPostSubscribed(viewer, post.getId())); + return dto; + } + + private void applyCommon(Post post, PostSummaryDto dto) { + dto.setId(post.getId()); + dto.setTitle(post.getTitle()); + dto.setContent(post.getContent()); + dto.setCreatedAt(post.getCreatedAt()); + dto.setAuthor(userMapper.toAuthorDto(post.getAuthor())); + dto.setCategory(categoryMapper.toDto(post.getCategory())); + dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList())); + dto.setViews(post.getViews()); + dto.setStatus(post.getStatus()); + dto.setPinnedAt(post.getPinnedAt()); + + List reactions = reactionService.getReactionsForPost(post.getId()) + .stream() + .map(reactionMapper::toDto) + .collect(Collectors.toList()); + dto.setReactions(reactions); + + List participants = commentService.getParticipants(post.getId(), 5); + dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); + + LocalDateTime last = commentService.getLastCommentTime(post.getId()); + dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); + dto.setReward(0); + dto.setSubscribed(false); + } +} diff --git a/backend/src/main/java/com/openisle/mapper/ReactionMapper.java b/backend/src/main/java/com/openisle/mapper/ReactionMapper.java new file mode 100644 index 000000000..d5cca8c90 --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/ReactionMapper.java @@ -0,0 +1,25 @@ +package com.openisle.mapper; + +import com.openisle.dto.ReactionDto; +import com.openisle.model.Reaction; +import org.springframework.stereotype.Component; + +/** Mapper for reactions. */ +@Component +public class ReactionMapper { + + public ReactionDto toDto(Reaction reaction) { + ReactionDto dto = new ReactionDto(); + dto.setId(reaction.getId()); + dto.setType(reaction.getType()); + dto.setUser(reaction.getUser().getUsername()); + if (reaction.getPost() != null) { + dto.setPostId(reaction.getPost().getId()); + } + if (reaction.getComment() != null) { + dto.setCommentId(reaction.getComment().getId()); + } + dto.setReward(0); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/TagMapper.java b/backend/src/main/java/com/openisle/mapper/TagMapper.java new file mode 100644 index 000000000..b19fbfffa --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/TagMapper.java @@ -0,0 +1,26 @@ +package com.openisle.mapper; + +import com.openisle.dto.TagDto; +import com.openisle.model.Tag; +import org.springframework.stereotype.Component; + +/** Mapper for tag entities. */ +@Component +public class TagMapper { + + public TagDto toDto(Tag tag) { + return toDto(tag, null); + } + + public TagDto toDto(Tag tag, Long count) { + TagDto dto = new TagDto(); + dto.setId(tag.getId()); + dto.setName(tag.getName()); + dto.setDescription(tag.getDescription()); + dto.setIcon(tag.getIcon()); + dto.setSmallIcon(tag.getSmallIcon()); + dto.setCreatedAt(tag.getCreatedAt()); + dto.setCount(count); + return dto; + } +} diff --git a/backend/src/main/java/com/openisle/mapper/UserMapper.java b/backend/src/main/java/com/openisle/mapper/UserMapper.java new file mode 100644 index 000000000..3edca1a49 --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/UserMapper.java @@ -0,0 +1,104 @@ +package com.openisle.mapper; + +import com.openisle.dto.*; +import com.openisle.model.Comment; +import com.openisle.model.Post; +import com.openisle.model.User; +import com.openisle.service.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +/** Mapper for user related DTOs. */ +@Component +@RequiredArgsConstructor +public class UserMapper { + + private final SubscriptionService subscriptionService; + private final PostService postService; + private final CommentService commentService; + private final ReactionService reactionService; + private final UserVisitService userVisitService; + private final PostReadService postReadService; + private final LevelService levelService; + + @Value("${app.snippet-length:50}") + private int snippetLength; + + public AuthorDto toAuthorDto(User user) { + AuthorDto dto = new AuthorDto(); + dto.setId(user.getId()); + dto.setUsername(user.getUsername()); + dto.setAvatar(user.getAvatar()); + return dto; + } + + public UserDto toDto(User user, Authentication viewer) { + UserDto dto = new UserDto(); + dto.setId(user.getId()); + dto.setUsername(user.getUsername()); + dto.setEmail(user.getEmail()); + dto.setAvatar(user.getAvatar()); + dto.setRole(user.getRole().name()); + dto.setIntroduction(user.getIntroduction()); + dto.setFollowers(subscriptionService.countSubscribers(user.getUsername())); + dto.setFollowing(subscriptionService.countSubscribed(user.getUsername())); + dto.setCreatedAt(user.getCreatedAt()); + dto.setLastPostTime(postService.getLastPostTime(user.getUsername())); + dto.setLastCommentTime(commentService.getLastCommentTimeOfUserByUserId(user.getId())); + dto.setTotalViews(postService.getTotalViews(user.getUsername())); + dto.setVisitedDays(userVisitService.countVisits(user.getUsername())); + dto.setReadPosts(postReadService.countReads(user.getUsername())); + dto.setLikesSent(reactionService.countLikesSent(user.getUsername())); + dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername())); + dto.setExperience(user.getExperience()); + dto.setCurrentLevel(levelService.getLevel(user.getExperience())); + dto.setNextLevelExp(levelService.nextLevelExp(user.getExperience())); + if (viewer != null) { + dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername())); + } else { + dto.setSubscribed(false); + } + return dto; + } + + public UserDto toDto(User user) { + return toDto(user, null); + } + + public PostMetaDto toMetaDto(Post post) { + PostMetaDto dto = new PostMetaDto(); + dto.setId(post.getId()); + dto.setTitle(post.getTitle()); + String content = post.getContent(); + if (content == null) { + content = ""; + } + if (snippetLength >= 0) { + dto.setSnippet(content.length() > snippetLength ? content.substring(0, snippetLength) : content); + } else { + dto.setSnippet(content); + } + dto.setCreatedAt(post.getCreatedAt()); + dto.setCategory(post.getCategory().getName()); + dto.setViews(post.getViews()); + return dto; + } + + public CommentInfoDto toCommentInfoDto(Comment comment) { + CommentInfoDto dto = new CommentInfoDto(); + dto.setId(comment.getId()); + dto.setContent(comment.getContent()); + dto.setCreatedAt(comment.getCreatedAt()); + dto.setPost(toMetaDto(comment.getPost())); + if (comment.getParent() != null) { + ParentCommentDto pc = new ParentCommentDto(); + pc.setId(comment.getParent().getId()); + pc.setAuthor(comment.getParent().getAuthor().getUsername()); + pc.setContent(comment.getParent().getContent()); + dto.setParentComment(pc); + } + return dto; + } +} diff --git a/backend/src/test/java/com/openisle/controller/PostControllerTest.java b/backend/src/test/java/com/openisle/controller/PostControllerTest.java index 7da8cdb25..d9c786e6b 100644 --- a/backend/src/test/java/com/openisle/controller/PostControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/PostControllerTest.java @@ -1,46 +1,36 @@ package com.openisle.controller; -import com.openisle.model.Post; -import com.openisle.model.User; -import com.openisle.model.Category; -import com.openisle.model.Tag; -import com.openisle.service.PostService; -import com.openisle.service.CommentService; -import com.openisle.service.ReactionService; -import com.openisle.service.CaptchaService; -import com.openisle.service.DraftService; -import com.openisle.service.LevelService; +import com.openisle.mapper.PostMapper; +import com.openisle.model.*; +import com.openisle.service.*; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.List; +import java.util.Set; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.util.ReflectionTestUtils; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.never; +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.*; @WebMvcTest(PostController.class) @AutoConfigureMockMvc(addFilters = false) +@Import(PostMapper.class) class PostControllerTest { @Autowired private MockMvc mockMvc; @Autowired private PostController postController; - @MockBean private PostService postService; @MockBean @@ -53,6 +43,10 @@ class PostControllerTest { private DraftService draftService; @MockBean private LevelService levelService; + @MockBean + private SubscriptionService subscriptionService; + @MockBean + private UserVisitService userVisitService; @Test void createAndGetPost() throws Exception { @@ -61,13 +55,9 @@ class PostControllerTest { Category cat = new Category(); cat.setId(1L); cat.setName("tech"); - cat.setDescription("d"); - cat.setIcon("i"); Tag tag = new Tag(); tag.setId(1L); tag.setName("java"); - tag.setDescription("td"); - tag.setIcon("ti"); Post post = new Post(); post.setId(1L); post.setTitle("t"); @@ -75,21 +65,70 @@ class PostControllerTest { post.setCreatedAt(LocalDateTime.now()); post.setAuthor(user); post.setCategory(cat); - post.setTags(java.util.Set.of(tag)); - Mockito.when(commentService.getParticipants(Mockito.anyLong(), Mockito.anyInt())).thenReturn(java.util.List.of()); - Mockito.when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(java.util.List.of(1L)))).thenReturn(post); - Mockito.when(postService.viewPost(eq(1L), Mockito.isNull())).thenReturn(post); + post.setTags(Set.of(tag)); + + when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)))).thenReturn(post); + when(postService.viewPost(eq(1L), any())).thenReturn(post); + when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of()); + when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); + when(reactionService.getReactionsForPost(1L)).thenReturn(List.of()); + when(commentService.getLastCommentTime(1L)).thenReturn(null); mockMvc.perform(post("/api/posts") .contentType("application/json") .content("{\"title\":\"t\",\"content\":\"c\",\"categoryId\":1,\"tagIds\":[1]}") .principal(new UsernamePasswordAuthenticationToken("alice", "p"))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.title").value("t")); + .andExpect(jsonPath("$.title").value("t")) + .andExpect(jsonPath("$.comments").isArray()) + .andExpect(jsonPath("$.comments").isEmpty()) + .andExpect(jsonPath("$.author.username").value("alice")) + .andExpect(jsonPath("$.category.name").value("tech")) + .andExpect(jsonPath("$.tags[0].name").value("java")) + .andExpect(jsonPath("$.subscribed").value(false)); mockMvc.perform(get("/api/posts/1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)); + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.comments").isArray()) + .andExpect(jsonPath("$.comments").isEmpty()) + .andExpect(jsonPath("$.subscribed").value(false)); + } + + @Test + void updatePostReturnsDetailDto() throws Exception { + User user = new User(); + user.setUsername("alice"); + Category cat = new Category(); + cat.setId(1L); + cat.setName("tech"); + Tag tag = new Tag(); + tag.setId(1L); + tag.setName("java"); + Post post = new Post(); + post.setId(1L); + post.setTitle("t2"); + post.setContent("c2"); + post.setCreatedAt(LocalDateTime.now()); + post.setAuthor(user); + post.setCategory(cat); + post.setTags(Set.of(tag)); + + when(postService.updatePost(eq(1L), eq("alice"), eq(1L), eq("t2"), eq("c2"), eq(List.of(1L)))).thenReturn(post); + when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of()); + when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); + when(reactionService.getReactionsForPost(1L)).thenReturn(List.of()); + when(commentService.getLastCommentTime(1L)).thenReturn(null); + + mockMvc.perform(put("/api/posts/1") + .contentType("application/json") + .content("{\"title\":\"t2\",\"content\":\"c2\",\"categoryId\":1,\"tagIds\":[1]}") + .principal(new UsernamePasswordAuthenticationToken("alice", "p"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("t2")) + .andExpect(jsonPath("$.comments").isArray()) + .andExpect(jsonPath("$.comments").isEmpty()) + .andExpect(jsonPath("$.author.username").value("alice")); } @Test @@ -99,13 +138,9 @@ class PostControllerTest { Category cat = new Category(); cat.setId(1L); cat.setName("tech"); - cat.setDescription("d"); - cat.setIcon("i"); Tag tag = new Tag(); tag.setId(1L); tag.setName("java"); - tag.setDescription("td"); - tag.setIcon("ti"); Post post = new Post(); post.setId(2L); post.setTitle("hello"); @@ -113,21 +148,28 @@ class PostControllerTest { post.setCreatedAt(LocalDateTime.now()); post.setAuthor(user); post.setCategory(cat); - post.setTags(java.util.Set.of(tag)); - Mockito.when(commentService.getParticipants(Mockito.anyLong(), Mockito.anyInt())).thenReturn(java.util.List.of()); - Mockito.when(postService.listPostsByCategories(Mockito.isNull(), Mockito.isNull(), Mockito.isNull())) - .thenReturn(List.of(post)); + post.setTags(Set.of(tag)); + + when(postService.listPostsByCategories(null, null, null)).thenReturn(List.of(post)); + when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); + when(reactionService.getReactionsForPost(anyLong())).thenReturn(List.of()); + when(commentService.getLastCommentTime(anyLong())).thenReturn(null); mockMvc.perform(get("/api/posts")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].title").value("hello")); + .andExpect(jsonPath("$[0].title").value("hello")) + .andExpect(jsonPath("$[0].comments").doesNotExist()) + .andExpect(jsonPath("$[0].author.username").value("bob")) + .andExpect(jsonPath("$[0].category.name").value("tech")) + .andExpect(jsonPath("$[0].tags[0].name").value("java")) + .andExpect(jsonPath("$[0].subscribed").value(false)); } @Test void createPostRejectsInvalidCaptcha() throws Exception { - org.springframework.test.util.ReflectionTestUtils.setField(postController, "captchaEnabled", true); - org.springframework.test.util.ReflectionTestUtils.setField(postController, "postCaptchaEnabled", true); - Mockito.when(captchaService.verify("bad")).thenReturn(false); + ReflectionTestUtils.setField(postController, "captchaEnabled", true); + ReflectionTestUtils.setField(postController, "postCaptchaEnabled", true); + when(captchaService.verify("bad")).thenReturn(false); mockMvc.perform(post("/api/posts") .contentType("application/json") @@ -155,7 +197,7 @@ class PostControllerTest { post.setCreatedAt(LocalDateTime.now()); post.setAuthor(user); post.setCategory(cat); - post.setTags(java.util.Set.of(tag)); + post.setTags(Set.of(tag)); com.openisle.model.Comment comment = new com.openisle.model.Comment(); comment.setId(2L); @@ -183,84 +225,55 @@ class PostControllerTest { cr.setComment(comment); cr.setType(com.openisle.model.ReactionType.LIKE); - Mockito.when(postService.viewPost(eq(1L), Mockito.isNull())).thenReturn(post); - Mockito.when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of(comment)); - Mockito.when(commentService.getReplies(2L)).thenReturn(List.of(reply)); - Mockito.when(commentService.getReplies(3L)).thenReturn(List.of()); - Mockito.when(commentService.getParticipants(Mockito.anyLong(), Mockito.anyInt())).thenReturn(java.util.List.of()); - Mockito.when(reactionService.getReactionsForPost(1L)).thenReturn(List.of(pr)); - Mockito.when(reactionService.getReactionsForComment(2L)).thenReturn(List.of(cr)); - Mockito.when(reactionService.getReactionsForComment(3L)).thenReturn(List.of()); + when(postService.viewPost(eq(1L), any())).thenReturn(post); + when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of(comment)); + when(commentService.getReplies(2L)).thenReturn(List.of(reply)); + when(commentService.getReplies(3L)).thenReturn(List.of()); + when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); + when(commentService.getLastCommentTime(1L)).thenReturn(null); + when(reactionService.getReactionsForPost(1L)).thenReturn(List.of(pr)); + when(reactionService.getReactionsForComment(2L)).thenReturn(List.of(cr)); + when(reactionService.getReactionsForComment(3L)).thenReturn(List.of()); mockMvc.perform(get("/api/posts/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.reactions[0].id").value(10)) .andExpect(jsonPath("$.comments[0].replies[0].id").value(3)) - .andExpect(jsonPath("$.comments[0].reactions[0].id").value(11)); + .andExpect(jsonPath("$.comments[0].reactions[0].id").value(11)) + .andExpect(jsonPath("$.author.username").value("alice")) + .andExpect(jsonPath("$.category.name").value("tech")) + .andExpect(jsonPath("$.tags[0].name").value("java")); } @Test - void listPostsByCategoriesAndTags() throws Exception { + void getPostSubscriptionStatus() throws Exception { User user = new User(); user.setUsername("alice"); Category cat = new Category(); + cat.setId(1L); cat.setName("tech"); - Tag tag = new Tag(); - tag.setId(1L); - tag.setName("java"); Post post = new Post(); - post.setId(2L); - post.setTitle("hello"); + post.setId(1L); + post.setTitle("t"); + post.setContent("c"); post.setCreatedAt(LocalDateTime.now()); post.setAuthor(user); post.setCategory(cat); - post.setTags(java.util.Set.of(tag)); - Mockito.when(commentService.getParticipants(Mockito.anyLong(), Mockito.anyInt())).thenReturn(java.util.List.of()); + post.setTags(Set.of()); - Mockito.when(postService.listPostsByCategoriesAndTags(eq(java.util.List.of(1L)), eq(java.util.List.of(1L, 2L)), eq(0), eq(5))) - .thenReturn(List.of(post)); + when(postService.viewPost(eq(1L), any())).thenReturn(post); + when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of()); + when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); + when(reactionService.getReactionsForPost(1L)).thenReturn(List.of()); + when(commentService.getLastCommentTime(1L)).thenReturn(null); + when(subscriptionService.isPostSubscribed("alice", 1L)).thenReturn(true); - mockMvc.perform(get("/api/posts") - .param("tagIds", "1,2") - .param("page", "0") - .param("pageSize", "5") - .param("categoryId", "1")) + mockMvc.perform(get("/api/posts/1").principal(new UsernamePasswordAuthenticationToken("alice", "p"))) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(2)); + .andExpect(jsonPath("$.subscribed").value(true)); - verify(postService).listPostsByCategoriesAndTags(eq(java.util.List.of(1L)), eq(java.util.List.of(1L, 2L)), eq(0), eq(5)); - verify(postService, never()).listPostsByCategories(any(), any(), any()); - } - - @Test - void rankingPostsFiltered() throws Exception { - User user = new User(); - user.setUsername("alice"); - Category cat = new Category(); - cat.setName("tech"); - Tag tag = new Tag(); - tag.setId(1L); - tag.setName("java"); - Post post = new Post(); - post.setId(3L); - post.setTitle("rank"); - post.setCreatedAt(LocalDateTime.now()); - post.setAuthor(user); - post.setCategory(cat); - post.setTags(java.util.Set.of(tag)); - Mockito.when(commentService.getParticipants(Mockito.anyLong(), Mockito.anyInt())).thenReturn(java.util.List.of()); - - Mockito.when(postService.listPostsByViews(eq(java.util.List.of(1L)), eq(java.util.List.of(1L, 2L)), eq(0), eq(5))) - .thenReturn(List.of(post)); - - mockMvc.perform(get("/api/posts/ranking") - .param("tagIds", "1,2") - .param("page", "0") - .param("pageSize", "5") - .param("categoryId", "1")) + mockMvc.perform(get("/api/posts/1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(3)); - - verify(postService).listPostsByViews(eq(java.util.List.of(1L)), eq(java.util.List.of(1L, 2L)), eq(0), eq(5)); + .andExpect(jsonPath("$.subscribed").value(false)); } } diff --git a/frontend/src/views/SignupReasonPageView.vue b/frontend/src/views/SignupReasonPageView.vue index 01af65116..3f76bbea3 100644 --- a/frontend/src/views/SignupReasonPageView.vue +++ b/frontend/src/views/SignupReasonPageView.vue @@ -6,7 +6,7 @@ 为了我们社区的良性发展,请填写注册理由,我们将根据你的理由审核你的注册, 谢谢!
- +
{{ reason.length }}/20
{{ error }}
@@ -18,11 +18,11 @@