diff --git a/open-isle-cli/src/views/ProfileView.vue b/open-isle-cli/src/views/ProfileView.vue index fd05a3afb..784c78932 100644 --- a/open-isle-cli/src/views/ProfileView.vue +++ b/open-isle-cli/src/views/ProfileView.vue @@ -64,19 +64,19 @@
访问天数
-
0
+
{{ user.visitedDays }}
已读帖子
-
165k
+
{{ user.readPosts }}
-
已送出的💗/div> -
165k
+
已送出的💗
+
{{ user.likesSent }}
已收到的💗
-
165k
+
{{ user.likesReceived }}
diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java index 9dae40e85..61802835b 100644 --- a/src/main/java/com/openisle/controller/PostController.java +++ b/src/main/java/com/openisle/controller/PostController.java @@ -9,6 +9,7 @@ import com.openisle.service.ReactionService; import com.openisle.service.CaptchaService; import com.openisle.service.DraftService; import com.openisle.service.SubscriptionService; +import com.openisle.service.UserVisitService; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -30,6 +31,7 @@ public class PostController { private final SubscriptionService subscriptionService; private final CaptchaService captchaService; private final DraftService draftService; + private final UserVisitService userVisitService; @Value("${app.captcha.enabled:false}") private boolean captchaEnabled; @@ -66,7 +68,8 @@ public class PostController { @RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagIds", required = false) List tagIds, @RequestParam(value = "page", required = false) Integer page, - @RequestParam(value = "pageSize", required = false) Integer pageSize) { + @RequestParam(value = "pageSize", required = false) Integer pageSize, + Authentication auth) { List ids = categoryIds; if (categoryId != null) { ids = java.util.List.of(categoryId); @@ -76,6 +79,10 @@ public class PostController { tids = java.util.List.of(tagId); } + if (auth != null) { + userVisitService.recordVisit(auth.getName()); + } + boolean hasCategories = ids != null && !ids.isEmpty(); boolean hasTags = tids != null && !tids.isEmpty(); @@ -98,7 +105,8 @@ public class PostController { @RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagIds", required = false) List tagIds, @RequestParam(value = "page", required = false) Integer page, - @RequestParam(value = "pageSize", required = false) Integer pageSize) { + @RequestParam(value = "pageSize", required = false) Integer pageSize, + Authentication auth) { List ids = categoryIds; if (categoryId != null) { ids = java.util.List.of(categoryId); @@ -108,6 +116,10 @@ public class PostController { tids = java.util.List.of(tagId); } + if (auth != null) { + userVisitService.recordVisit(auth.getName()); + } + return postService.listPostsByViews(ids, tids, page, pageSize) .stream().map(this::toDto).collect(Collectors.toList()); } diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java index 9c5601cdc..9d90bcd1a 100644 --- a/src/main/java/com/openisle/controller/UserController.java +++ b/src/main/java/com/openisle/controller/UserController.java @@ -23,6 +23,8 @@ public class UserController { private final CommentService commentService; private final ReactionService reactionService; private final SubscriptionService subscriptionService; + private final PostReadService postReadService; + private final UserVisitService userVisitService; @Value("${app.upload.check-type:true}") private boolean checkImageType; @@ -170,6 +172,10 @@ public class UserController { dto.setCreatedAt(user.getCreatedAt()); dto.setLastPostTime(postService.getLastPostTime(user.getUsername())); 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())); if (viewer != null) { dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername())); } else { @@ -230,6 +236,10 @@ public class UserController { private java.time.LocalDateTime createdAt; private java.time.LocalDateTime lastPostTime; private long totalViews; + private long visitedDays; + private long readPosts; + private long likesSent; + private long likesReceived; private boolean subscribed; } diff --git a/src/main/java/com/openisle/model/PostRead.java b/src/main/java/com/openisle/model/PostRead.java new file mode 100644 index 000000000..fd71ba0b3 --- /dev/null +++ b/src/main/java/com/openisle/model/PostRead.java @@ -0,0 +1,32 @@ +package com.openisle.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** Record of a user reading a post. */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "post_reads", + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"})) +public class PostRead { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "post_id") + private Post post; + + @Column(name = "last_read_at") + private LocalDateTime lastReadAt; +} diff --git a/src/main/java/com/openisle/model/UserVisit.java b/src/main/java/com/openisle/model/UserVisit.java new file mode 100644 index 000000000..6c2ecab0e --- /dev/null +++ b/src/main/java/com/openisle/model/UserVisit.java @@ -0,0 +1,28 @@ +package com.openisle.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +/** Daily visit record for a user. */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "user_visits", + uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "visit_date"})) +public class UserVisit { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "visit_date", nullable = false) + private LocalDate visitDate; +} diff --git a/src/main/java/com/openisle/repository/PostReadRepository.java b/src/main/java/com/openisle/repository/PostReadRepository.java new file mode 100644 index 000000000..a158962f9 --- /dev/null +++ b/src/main/java/com/openisle/repository/PostReadRepository.java @@ -0,0 +1,13 @@ +package com.openisle.repository; + +import com.openisle.model.Post; +import com.openisle.model.PostRead; +import com.openisle.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostReadRepository extends JpaRepository { + Optional findByUserAndPost(User user, Post post); + long countByUser(User user); +} diff --git a/src/main/java/com/openisle/repository/ReactionRepository.java b/src/main/java/com/openisle/repository/ReactionRepository.java index c5ba190ac..3c36df37d 100644 --- a/src/main/java/com/openisle/repository/ReactionRepository.java +++ b/src/main/java/com/openisle/repository/ReactionRepository.java @@ -23,4 +23,10 @@ public interface ReactionRepository extends JpaRepository { @Query("SELECT r.comment.id FROM Reaction r WHERE r.comment IS NOT NULL AND r.comment.author.username = :username AND r.type = com.openisle.model.ReactionType.LIKE GROUP BY r.comment.id ORDER BY COUNT(r.id) DESC") List findTopCommentIds(@Param("username") String username, Pageable pageable); + + @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.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/repository/UserVisitRepository.java b/src/main/java/com/openisle/repository/UserVisitRepository.java new file mode 100644 index 000000000..bf5870fbc --- /dev/null +++ b/src/main/java/com/openisle/repository/UserVisitRepository.java @@ -0,0 +1,13 @@ +package com.openisle.repository; + +import com.openisle.model.User; +import com.openisle.model.UserVisit; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.Optional; + +public interface UserVisitRepository extends JpaRepository { + Optional findByUserAndVisitDate(User user, LocalDate visitDate); + long countByUser(User user); +} diff --git a/src/main/java/com/openisle/service/PostReadService.java b/src/main/java/com/openisle/service/PostReadService.java new file mode 100644 index 000000000..c44ef2b1e --- /dev/null +++ b/src/main/java/com/openisle/service/PostReadService.java @@ -0,0 +1,44 @@ +package com.openisle.service; + +import com.openisle.model.Post; +import com.openisle.model.PostRead; +import com.openisle.model.User; +import com.openisle.repository.PostReadRepository; +import com.openisle.repository.PostRepository; +import com.openisle.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class PostReadService { + private final PostReadRepository postReadRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + + public void recordRead(String username, Long postId) { + if (username == null) return; + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("Post not found")); + postReadRepository.findByUserAndPost(user, post).ifPresentOrElse(pr -> { + pr.setLastReadAt(LocalDateTime.now()); + postReadRepository.save(pr); + }, () -> { + PostRead pr = new PostRead(); + pr.setUser(user); + pr.setPost(post); + pr.setLastReadAt(LocalDateTime.now()); + postReadRepository.save(pr); + }); + } + + public long countReads(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + return postReadRepository.countByUser(user); + } +} diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java index 5d85578b8..a3c39e10d 100644 --- a/src/main/java/com/openisle/service/PostService.java +++ b/src/main/java/com/openisle/service/PostService.java @@ -39,6 +39,7 @@ public class PostService { private final ReactionRepository reactionRepository; private final PostSubscriptionRepository postSubscriptionRepository; private final NotificationRepository notificationRepository; + private final PostReadService postReadService; @org.springframework.beans.factory.annotation.Autowired public PostService(PostRepository postRepository, @@ -52,6 +53,7 @@ public class PostService { ReactionRepository reactionRepository, PostSubscriptionRepository postSubscriptionRepository, NotificationRepository notificationRepository, + PostReadService postReadService, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) { this.postRepository = postRepository; this.userRepository = userRepository; @@ -64,6 +66,7 @@ public class PostService { this.reactionRepository = reactionRepository; this.postSubscriptionRepository = postSubscriptionRepository; this.notificationRepository = notificationRepository; + this.postReadService = postReadService; this.publishMode = publishMode; } @@ -142,6 +145,9 @@ public class PostService { } post.setViews(post.getViews() + 1); post = postRepository.save(post); + if (viewer != null) { + postReadService.recordRead(viewer, id); + } if (viewer != null && !viewer.equals(post.getAuthor().getUsername())) { User viewerUser = userRepository.findByUsername(viewer).orElse(null); if (viewerUser != null) { diff --git a/src/main/java/com/openisle/service/ReactionService.java b/src/main/java/com/openisle/service/ReactionService.java index b51d418c0..104db91c3 100644 --- a/src/main/java/com/openisle/service/ReactionService.java +++ b/src/main/java/com/openisle/service/ReactionService.java @@ -87,4 +87,12 @@ public class ReactionService { public java.util.List topCommentIds(String username, int limit) { return reactionRepository.findTopCommentIds(username, org.springframework.data.domain.PageRequest.of(0, limit)); } + + public long countLikesSent(String username) { + return reactionRepository.countLikesSent(username); + } + + public long countLikesReceived(String username) { + return reactionRepository.countLikesReceived(username); + } } diff --git a/src/main/java/com/openisle/service/UserVisitService.java b/src/main/java/com/openisle/service/UserVisitService.java new file mode 100644 index 000000000..305c860c3 --- /dev/null +++ b/src/main/java/com/openisle/service/UserVisitService.java @@ -0,0 +1,35 @@ +package com.openisle.service; + +import com.openisle.model.User; +import com.openisle.model.UserVisit; +import com.openisle.repository.UserRepository; +import com.openisle.repository.UserVisitRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +public class UserVisitService { + private final UserVisitRepository userVisitRepository; + private final UserRepository userRepository; + + public void recordVisit(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + LocalDate today = LocalDate.now(); + userVisitRepository.findByUserAndVisitDate(user, today).orElseGet(() -> { + UserVisit visit = new UserVisit(); + visit.setUser(user); + visit.setVisitDate(today); + return userVisitRepository.save(visit); + }); + } + + public long countVisits(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + return userVisitRepository.countByUser(user); + } +}