diff --git a/open-isle-cli/src/components/ReactionsGroup.vue b/open-isle-cli/src/components/ReactionsGroup.vue index cc6db68a6..a35c0c2b9 100644 --- a/open-isle-cli/src/components/ReactionsGroup.vue +++ b/open-isle-cli/src/components/ReactionsGroup.vue @@ -144,6 +144,9 @@ export default { } else { const data = await res.json() reactions.value.push(data) + if (data.reward && data.reward > 0) { + toast.success(`获得 ${data.reward} 经验值`) + } } emit('update:modelValue', reactions.value) } else { diff --git a/open-isle-cli/src/views/NewPostPageView.vue b/open-isle-cli/src/views/NewPostPageView.vue index 6b3e84ca1..d70066e57 100644 --- a/open-isle-cli/src/views/NewPostPageView.vue +++ b/open-isle-cli/src/views/NewPostPageView.vue @@ -232,7 +232,11 @@ export default { }) const data = await res.json() if (res.ok) { - toast.success('发布成功') + if (data.reward && data.reward > 0) { + toast.success(`发布成功,获得 ${data.reward} 经验值`) + } else { + toast.success('发布成功') + } if (data.id) { window.location.href = `/posts/${data.id}` } diff --git a/open-isle-cli/src/views/PostPageView.vue b/open-isle-cli/src/views/PostPageView.vue index fab4258e7..47de34d15 100644 --- a/open-isle-cli/src/views/PostPageView.vue +++ b/open-isle-cli/src/views/PostPageView.vue @@ -354,7 +354,11 @@ export default { comments.value.push(mapComment(data)) await nextTick() gatherPostItems() - toast.success('评论成功') + if (data.reward && data.reward > 0) { + toast.success(`评论成功,获得 ${data.reward} 经验值`) + } else { + toast.success('评论成功') + } } else { toast.error('评论失败') } diff --git a/src/main/java/com/openisle/controller/CommentController.java b/src/main/java/com/openisle/controller/CommentController.java index 99032228a..c3aab14d1 100644 --- a/src/main/java/com/openisle/controller/CommentController.java +++ b/src/main/java/com/openisle/controller/CommentController.java @@ -3,6 +3,7 @@ package com.openisle.controller; import com.openisle.model.Comment; import com.openisle.service.CommentService; import com.openisle.service.CaptchaService; +import com.openisle.service.LevelService; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,6 +20,7 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class CommentController { private final CommentService commentService; + private final LevelService levelService; private final CaptchaService captchaService; @Value("${app.captcha.enabled:false}") @@ -35,7 +37,9 @@ public class CommentController { return ResponseEntity.badRequest().build(); } Comment comment = commentService.addComment(auth.getName(), postId, req.getContent()); - return ResponseEntity.ok(toDto(comment)); + CommentDto dto = toDto(comment); + dto.setReward(levelService.awardForComment(auth.getName())); + return ResponseEntity.ok(dto); } @PostMapping("/comments/{commentId}/replies") @@ -46,7 +50,9 @@ public class CommentController { return ResponseEntity.badRequest().build(); } Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent()); - return ResponseEntity.ok(toDto(comment)); + CommentDto dto = toDto(comment); + dto.setReward(levelService.awardForComment(auth.getName())); + return ResponseEntity.ok(dto); } @GetMapping("/posts/{postId}/comments") @@ -76,6 +82,7 @@ public class CommentController { dto.setContent(comment.getContent()); dto.setCreatedAt(comment.getCreatedAt()); dto.setAuthor(toAuthorDto(comment.getAuthor())); + dto.setReward(0); return dto; } @@ -100,6 +107,7 @@ public class CommentController { private LocalDateTime createdAt; private AuthorDto author; private List replies; + private int reward; } @Data diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java index 20d5ef5b7..c839c2173 100644 --- a/src/main/java/com/openisle/controller/PostController.java +++ b/src/main/java/com/openisle/controller/PostController.java @@ -10,6 +10,7 @@ import com.openisle.service.CaptchaService; import com.openisle.service.DraftService; import com.openisle.service.SubscriptionService; import com.openisle.service.UserVisitService; +import com.openisle.service.LevelService; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -29,6 +30,7 @@ public class PostController { 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; @@ -47,7 +49,9 @@ public class PostController { Post post = postService.createPost(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); draftService.deleteDraft(auth.getName()); - return ResponseEntity.ok(toDto(post)); + PostDto dto = toDto(post); + dto.setReward(levelService.awardForPost(auth.getName())); + return ResponseEntity.ok(dto); } @PutMapping("/{id}") @@ -187,6 +191,7 @@ public class PostController { java.time.LocalDateTime last = commentService.getLastCommentTime(post.getId()); dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); + dto.setReward(0); return dto; } @@ -223,6 +228,7 @@ public class PostController { dto.setContent(comment.getContent()); dto.setCreatedAt(comment.getCreatedAt()); dto.setAuthor(toAuthorDto(comment.getAuthor())); + dto.setReward(0); return dto; } @@ -237,6 +243,7 @@ public class PostController { if (reaction.getComment() != null) { dto.setCommentId(reaction.getComment().getId()); } + dto.setReward(0); return dto; } @@ -294,6 +301,7 @@ public class PostController { private List reactions; private java.util.List participants; private boolean subscribed; + private int reward; } @Data @@ -329,6 +337,7 @@ public class PostController { private AuthorDto author; private List replies; private List reactions; + private int reward; } @Data @@ -338,5 +347,6 @@ public class PostController { private String user; private Long postId; private Long commentId; + private int reward; } } diff --git a/src/main/java/com/openisle/controller/ReactionController.java b/src/main/java/com/openisle/controller/ReactionController.java index ef8ddd1fa..c762486ab 100644 --- a/src/main/java/com/openisle/controller/ReactionController.java +++ b/src/main/java/com/openisle/controller/ReactionController.java @@ -3,6 +3,7 @@ package com.openisle.controller; import com.openisle.model.Reaction; import com.openisle.model.ReactionType; import com.openisle.service.ReactionService; +import com.openisle.service.LevelService; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor public class ReactionController { private final ReactionService reactionService; + private final LevelService levelService; /** * Get all available reaction types. @@ -31,7 +33,9 @@ public class ReactionController { if (reaction == null) { return ResponseEntity.noContent().build(); } - return ResponseEntity.ok(toDto(reaction)); + ReactionDto dto = toDto(reaction); + dto.setReward(levelService.awardForReaction(auth.getName())); + return ResponseEntity.ok(dto); } @PostMapping("/comments/{commentId}/reactions") @@ -42,7 +46,9 @@ public class ReactionController { if (reaction == null) { return ResponseEntity.noContent().build(); } - return ResponseEntity.ok(toDto(reaction)); + ReactionDto dto = toDto(reaction); + dto.setReward(levelService.awardForReaction(auth.getName())); + return ResponseEntity.ok(dto); } private ReactionDto toDto(Reaction reaction) { @@ -56,6 +62,7 @@ public class ReactionController { if (reaction.getComment() != null) { dto.setCommentId(reaction.getComment().getId()); } + dto.setReward(0); return dto; } @@ -71,5 +78,6 @@ public class ReactionController { private String user; private Long postId; private Long commentId; + private int reward; } } diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java index 4b53d6236..1ffddcb38 100644 --- a/src/main/java/com/openisle/controller/UserController.java +++ b/src/main/java/com/openisle/controller/UserController.java @@ -27,6 +27,7 @@ public class UserController { private final SubscriptionService subscriptionService; private final PostReadService postReadService; private final UserVisitService userVisitService; + private final LevelService levelService; @Value("${app.upload.check-type:true}") private boolean checkImageType; @@ -78,6 +79,12 @@ public class UserController { return ResponseEntity.ok(toDto(user, auth)); } + @PostMapping("/me/signin") + public Map signIn(Authentication auth) { + int reward = levelService.awardForSignin(auth.getName()); + return Map.of("reward", reward); + } + @GetMapping("/{identifier}") public ResponseEntity getUser(@PathVariable("identifier") String identifier, Authentication auth) { @@ -223,6 +230,9 @@ public class UserController { 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 { @@ -288,6 +298,9 @@ public class UserController { private long likesSent; private long likesReceived; private boolean subscribed; + private int experience; + private int currentLevel; + private int nextLevelExp; } @Data diff --git a/src/main/java/com/openisle/model/Reaction.java b/src/main/java/com/openisle/model/Reaction.java index 9fb71d8c1..b2610e8df 100644 --- a/src/main/java/com/openisle/model/Reaction.java +++ b/src/main/java/com/openisle/model/Reaction.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; /** * Reaction entity representing a user's reaction to a post or comment. @@ -37,4 +38,9 @@ public class Reaction { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "comment_id") private Comment comment; + + @CreationTimestamp + @Column(nullable = false, updatable = false, + columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)") + private java.time.LocalDateTime createdAt; } diff --git a/src/main/java/com/openisle/model/User.java b/src/main/java/com/openisle/model/User.java index dcc8516bb..96abf5882 100644 --- a/src/main/java/com/openisle/model/User.java +++ b/src/main/java/com/openisle/model/User.java @@ -39,6 +39,9 @@ public class User { private String avatar; + @Column(nullable = false) + private int experience = 0; + @Column(length = 1000) private String introduction; diff --git a/src/main/java/com/openisle/repository/CommentRepository.java b/src/main/java/com/openisle/repository/CommentRepository.java index 07ab209e8..1b4b8e092 100644 --- a/src/main/java/com/openisle/repository/CommentRepository.java +++ b/src/main/java/com/openisle/repository/CommentRepository.java @@ -19,4 +19,8 @@ public interface CommentRepository extends JpaRepository { @org.springframework.data.jpa.repository.Query("SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post") java.time.LocalDateTime findLastCommentTime(@org.springframework.data.repository.query.Param("post") Post post); + + @org.springframework.data.jpa.repository.Query("SELECT COUNT(c) FROM Comment c WHERE c.author.username = :username AND c.createdAt >= :start") + long countByAuthorAfter(@org.springframework.data.repository.query.Param("username") String username, + @org.springframework.data.repository.query.Param("start") java.time.LocalDateTime start); } diff --git a/src/main/java/com/openisle/repository/PostRepository.java b/src/main/java/com/openisle/repository/PostRepository.java index b7bde987f..bfe410dc2 100644 --- a/src/main/java/com/openisle/repository/PostRepository.java +++ b/src/main/java/com/openisle/repository/PostRepository.java @@ -87,6 +87,9 @@ public interface PostRepository extends JpaRepository { @Query("SELECT SUM(p.views) FROM Post p WHERE p.author.username = :username AND p.status = com.openisle.model.PostStatus.PUBLISHED") Long sumViews(@Param("username") String username); + @Query("SELECT COUNT(p) FROM Post p WHERE p.author.username = :username AND p.createdAt >= :start") + long countByAuthorAfter(@Param("username") String username, @Param("start") java.time.LocalDateTime start); + long countByCategory_Id(Long categoryId); long countDistinctByTags_Id(Long tagId); diff --git a/src/main/java/com/openisle/repository/ReactionRepository.java b/src/main/java/com/openisle/repository/ReactionRepository.java index 3c36df37d..c5055af55 100644 --- a/src/main/java/com/openisle/repository/ReactionRepository.java +++ b/src/main/java/com/openisle/repository/ReactionRepository.java @@ -27,6 +27,9 @@ public interface ReactionRepository extends JpaRepository { @Query("SELECT COUNT(r) FROM Reaction r WHERE r.user.username = :username AND r.type = com.openisle.model.ReactionType.LIKE") long countLikesSent(@Param("username") String username); + @Query("SELECT COUNT(r) FROM Reaction r WHERE r.user.username = :username AND r.createdAt >= :start") + long countByUserAfter(@Param("username") String username, @Param("start") java.time.LocalDateTime start); + @Query("SELECT COUNT(r) FROM Reaction r WHERE r.type = com.openisle.model.ReactionType.LIKE AND ((r.post IS NOT NULL AND r.post.author.username = :username) OR (r.comment IS NOT NULL AND r.comment.author.username = :username))") long countLikesReceived(@Param("username") String username); } diff --git a/src/main/java/com/openisle/service/LevelService.java b/src/main/java/com/openisle/service/LevelService.java new file mode 100644 index 000000000..12e43d8c2 --- /dev/null +++ b/src/main/java/com/openisle/service/LevelService.java @@ -0,0 +1,76 @@ +package com.openisle.service; + +import com.openisle.model.User; +import com.openisle.repository.CommentRepository; +import com.openisle.repository.PostRepository; +import com.openisle.repository.ReactionRepository; +import com.openisle.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class LevelService { + private final UserRepository userRepository; + private final PostRepository postRepository; + private final CommentRepository commentRepository; + private final ReactionRepository reactionRepository; + private final UserVisitService userVisitService; + + private static final int[] LEVEL_EXP = {100,200,300,600,1200,10000}; + + private int addExperience(User user, int amount) { + user.setExperience(user.getExperience() + amount); + userRepository.save(user); + return amount; + } + + public int awardForPost(String username) { + User user = userRepository.findByUsername(username).orElseThrow(); + LocalDateTime start = LocalDate.now().atStartOfDay(); + long count = postRepository.countByAuthorAfter(username, start); + if (count >= 1) return 0; + return addExperience(user,30); + } + + public int awardForComment(String username) { + User user = userRepository.findByUsername(username).orElseThrow(); + LocalDateTime start = LocalDate.now().atStartOfDay(); + long count = commentRepository.countByAuthorAfter(username, start); + if (count >= 3) return 0; + return addExperience(user,10); + } + + public int awardForReaction(String username) { + User user = userRepository.findByUsername(username).orElseThrow(); + LocalDateTime start = LocalDate.now().atStartOfDay(); + long count = reactionRepository.countByUserAfter(username, start); + if (count >= 3) return 0; + return addExperience(user,5); + } + + public int awardForSignin(String username) { + boolean first = userVisitService.recordVisit(username); + if (!first) return 0; + User user = userRepository.findByUsername(username).orElseThrow(); + return addExperience(user,5); + } + + public int getLevel(int exp) { + int level = 0; + for (int t : LEVEL_EXP) { + if (exp >= t) level++; else break; + } + return level; + } + + public int nextLevelExp(int exp) { + for (int t : LEVEL_EXP) { + if (exp < t) return t; + } + return LEVEL_EXP[LEVEL_EXP.length-1]; + } +} diff --git a/src/main/java/com/openisle/service/UserVisitService.java b/src/main/java/com/openisle/service/UserVisitService.java index 54ab2db35..3ad8339f9 100644 --- a/src/main/java/com/openisle/service/UserVisitService.java +++ b/src/main/java/com/openisle/service/UserVisitService.java @@ -17,15 +17,16 @@ public class UserVisitService { private final UserVisitRepository userVisitRepository; private final UserRepository userRepository; - public void recordVisit(String username) { + public boolean recordVisit(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); LocalDate today = LocalDate.now(); - userVisitRepository.findByUserAndVisitDate(user, today).orElseGet(() -> { + return userVisitRepository.findByUserAndVisitDate(user, today).map(v -> false).orElseGet(() -> { UserVisit visit = new UserVisit(); visit.setUser(user); visit.setVisitDate(today); - return userVisitRepository.save(visit); + userVisitRepository.save(visit); + return true; }); } diff --git a/src/test/java/com/openisle/controller/CommentControllerTest.java b/src/test/java/com/openisle/controller/CommentControllerTest.java index dd6ac95b3..959e8b90e 100644 --- a/src/test/java/com/openisle/controller/CommentControllerTest.java +++ b/src/test/java/com/openisle/controller/CommentControllerTest.java @@ -5,6 +5,7 @@ import com.openisle.model.Post; import com.openisle.model.User; import com.openisle.service.CommentService; import com.openisle.service.CaptchaService; +import com.openisle.service.LevelService; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +34,8 @@ class CommentControllerTest { private CommentService commentService; @MockBean private CaptchaService captchaService; + @MockBean + private LevelService levelService; private Comment createComment(Long id, String content, String authorName) { User user = new User(); diff --git a/src/test/java/com/openisle/controller/PostControllerTest.java b/src/test/java/com/openisle/controller/PostControllerTest.java index 7f221bb5e..6a46af268 100644 --- a/src/test/java/com/openisle/controller/PostControllerTest.java +++ b/src/test/java/com/openisle/controller/PostControllerTest.java @@ -9,6 +9,7 @@ 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 org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -50,6 +51,8 @@ class PostControllerTest { private CaptchaService captchaService; @MockBean private DraftService draftService; + @MockBean + private LevelService levelService; @Test void createAndGetPost() throws Exception { diff --git a/src/test/java/com/openisle/controller/ReactionControllerTest.java b/src/test/java/com/openisle/controller/ReactionControllerTest.java index 1ebf17fbc..697a2dfd0 100644 --- a/src/test/java/com/openisle/controller/ReactionControllerTest.java +++ b/src/test/java/com/openisle/controller/ReactionControllerTest.java @@ -6,6 +6,7 @@ import com.openisle.model.Reaction; import com.openisle.model.ReactionType; import com.openisle.model.User; import com.openisle.service.ReactionService; +import com.openisle.service.LevelService; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +30,8 @@ class ReactionControllerTest { @MockBean private ReactionService reactionService; + @MockBean + private LevelService levelService; @Test void reactToPost() throws Exception { diff --git a/src/test/java/com/openisle/controller/UserControllerTest.java b/src/test/java/com/openisle/controller/UserControllerTest.java index 86bf92896..11124f0b9 100644 --- a/src/test/java/com/openisle/controller/UserControllerTest.java +++ b/src/test/java/com/openisle/controller/UserControllerTest.java @@ -38,6 +38,8 @@ class UserControllerTest { private PostService postService; @MockBean private CommentService commentService; + @MockBean + private LevelService levelService; @Test void getCurrentUser() throws Exception {