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);
+ }
+}