mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-24 15:10:48 +08:00
Compare commits
19 Commits
codex/upda
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b36b5b59dc | ||
|
|
cf96806f80 | ||
|
|
3d0d0496b6 | ||
|
|
f67e220894 | ||
|
|
9306e35b84 | ||
|
|
d2268a1944 | ||
|
|
6baa4d4233 | ||
|
|
ef9d90455f | ||
|
|
5d499956d7 | ||
|
|
9101ed336c | ||
|
|
28e3ebb911 | ||
|
|
e93e33fe43 | ||
|
|
0ebeccf21e | ||
|
|
89842b82e9 | ||
|
|
58594229f2 | ||
|
|
b4a811ff4e | ||
|
|
7067630bcc | ||
|
|
b28e8d4bc9 | ||
|
|
1a21ba8935 |
@@ -97,8 +97,10 @@ public class CachingConfig {
|
|||||||
// 个别缓存单独设置 TTL 时间
|
// 个别缓存单独设置 TTL 时间
|
||||||
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
|
||||||
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1));
|
||||||
|
RedisCacheConfiguration tenMinutesConfig = config.entryTtl(Duration.ofMinutes(10));
|
||||||
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
|
||||||
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
|
||||||
|
cacheConfigs.put(POST_CACHE_NAME, tenMinutesConfig);
|
||||||
|
|
||||||
return RedisCacheManager.builder(connectionFactory)
|
return RedisCacheManager.builder(connectionFactory)
|
||||||
.cacheDefaults(config)
|
.cacheDefaults(config)
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ public class PostMapper {
|
|||||||
dto.setCategory(categoryMapper.toDto(post.getCategory()));
|
dto.setCategory(categoryMapper.toDto(post.getCategory()));
|
||||||
dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList()));
|
dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList()));
|
||||||
dto.setViews(post.getViews());
|
dto.setViews(post.getViews());
|
||||||
dto.setCommentCount(commentService.countComments(post.getId()));
|
|
||||||
dto.setStatus(post.getStatus());
|
dto.setStatus(post.getStatus());
|
||||||
dto.setPinnedAt(post.getPinnedAt());
|
dto.setPinnedAt(post.getPinnedAt());
|
||||||
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
||||||
@@ -82,8 +81,12 @@ public class PostMapper {
|
|||||||
List<User> participants = commentService.getParticipants(post.getId(), 5);
|
List<User> participants = commentService.getParticipants(post.getId(), 5);
|
||||||
dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||||
|
|
||||||
LocalDateTime last = commentService.getLastCommentTime(post.getId());
|
LocalDateTime last = post.getLastReplyAt();
|
||||||
dto.setLastReplyAt(last != null ? last : post.getCreatedAt());
|
if (last == null) {
|
||||||
|
commentService.updatePostCommentStats(post);
|
||||||
|
}
|
||||||
|
dto.setCommentCount(post.getCommentCount());
|
||||||
|
dto.setLastReplyAt(post.getLastReplyAt());
|
||||||
dto.setReward(0);
|
dto.setReward(0);
|
||||||
dto.setSubscribed(false);
|
dto.setSubscribed(false);
|
||||||
dto.setType(post.getType());
|
dto.setType(post.getType());
|
||||||
|
|||||||
@@ -72,4 +72,10 @@ public class Post {
|
|||||||
|
|
||||||
@Column(nullable = true)
|
@Column(nullable = true)
|
||||||
private Boolean rssExcluded = true;
|
private Boolean rssExcluded = true;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private long commentCount = 0;
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
private LocalDateTime lastReplyAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.openisle.repository;
|
package com.openisle.repository;
|
||||||
|
|
||||||
import com.openisle.model.PointHistory;
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import com.openisle.model.Comment;
|
import com.openisle.model.Comment;
|
||||||
|
import com.openisle.model.PointHistory;
|
||||||
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.model.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -14,6 +15,8 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
|
|||||||
long countByUser(User user);
|
long countByUser(User user);
|
||||||
|
|
||||||
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
||||||
|
|
||||||
List<PointHistory> findByComment(Comment comment);
|
List<PointHistory> findByComment(Comment comment);
|
||||||
|
|
||||||
|
List<PointHistory> findByPost(Post post);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
|
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
|
||||||
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
|
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
|
||||||
|
|
||||||
|
void deleteByPost(Post post);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ public class CommentService {
|
|||||||
comment.setContent(content);
|
comment.setContent(content);
|
||||||
comment = commentRepository.save(comment);
|
comment = commentRepository.save(comment);
|
||||||
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
||||||
|
|
||||||
|
// Update post comment statistics
|
||||||
|
updatePostCommentStats(post);
|
||||||
|
|
||||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||||
if (!author.getId().equals(post.getAuthor().getId())) {
|
if (!author.getId().equals(post.getAuthor().getId())) {
|
||||||
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
|
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
|
||||||
@@ -129,6 +133,10 @@ public class CommentService {
|
|||||||
comment.setContent(content);
|
comment.setContent(content);
|
||||||
comment = commentRepository.save(comment);
|
comment = commentRepository.save(comment);
|
||||||
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
||||||
|
|
||||||
|
// Update post comment statistics
|
||||||
|
updatePostCommentStats(parent.getPost());
|
||||||
|
|
||||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||||
if (!author.getId().equals(parent.getAuthor().getId())) {
|
if (!author.getId().equals(parent.getAuthor().getId())) {
|
||||||
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
|
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
|
||||||
@@ -282,9 +290,13 @@ public class CommentService {
|
|||||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||||
|
|
||||||
// 逻辑删除评论
|
// 逻辑删除评论
|
||||||
|
Post post = comment.getPost();
|
||||||
commentRepository.delete(comment);
|
commentRepository.delete(comment);
|
||||||
// 删除积分历史
|
// 删除积分历史
|
||||||
pointHistoryRepository.deleteAll(pointHistories);
|
pointHistoryRepository.deleteAll(pointHistories);
|
||||||
|
|
||||||
|
// Update post comment statistics
|
||||||
|
updatePostCommentStats(post);
|
||||||
|
|
||||||
// 重新计算受影响用户的积分
|
// 重新计算受影响用户的积分
|
||||||
if (!usersToRecalculate.isEmpty()) {
|
if (!usersToRecalculate.isEmpty()) {
|
||||||
@@ -330,4 +342,23 @@ public class CommentService {
|
|||||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||||
return reactions + replies;
|
return reactions + replies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update post comment statistics (comment count and last reply time)
|
||||||
|
*/
|
||||||
|
public void updatePostCommentStats(Post post) {
|
||||||
|
long commentCount = commentRepository.countByPostId(post.getId());
|
||||||
|
post.setCommentCount(commentCount);
|
||||||
|
|
||||||
|
LocalDateTime lastReplyAt = commentRepository.findLastCommentTime(post);
|
||||||
|
if (lastReplyAt == null) {
|
||||||
|
post.setLastReplyAt(post.getCreatedAt());
|
||||||
|
} else {
|
||||||
|
post.setLastReplyAt(lastReplyAt);
|
||||||
|
}
|
||||||
|
postRepository.save(post);
|
||||||
|
|
||||||
|
log.debug("Updated post {} stats: commentCount={}, lastReplyAt={}",
|
||||||
|
post.getId(), commentCount, lastReplyAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ public class PostChangeLogService {
|
|||||||
logRepository.save(log);
|
logRepository.save(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteLogsForPost(Post post) {
|
||||||
|
logRepository.deleteByPost(post);
|
||||||
|
}
|
||||||
|
|
||||||
public List<PostChangeLog> listLogs(Long postId) {
|
public List<PostChangeLog> listLogs(Long postId) {
|
||||||
Post post = postRepository.findById(postId)
|
Post post = postRepository.findById(postId)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import com.openisle.repository.PollPostRepository;
|
|||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.repository.CategoryRepository;
|
import com.openisle.repository.CategoryRepository;
|
||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.service.SubscriptionService;
|
|
||||||
import com.openisle.service.CommentService;
|
|
||||||
import com.openisle.service.PostChangeLogService;
|
|
||||||
import com.openisle.repository.CommentRepository;
|
import com.openisle.repository.CommentRepository;
|
||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.PostSubscriptionRepository;
|
import com.openisle.repository.PostSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
import com.openisle.repository.PollVoteRepository;
|
import com.openisle.repository.PollVoteRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -74,6 +72,7 @@ public class PostService {
|
|||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
private final PostChangeLogService postChangeLogService;
|
private final PostChangeLogService postChangeLogService;
|
||||||
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
@@ -102,6 +101,7 @@ public class PostService {
|
|||||||
ApplicationContext applicationContext,
|
ApplicationContext applicationContext,
|
||||||
PointService pointService,
|
PointService pointService,
|
||||||
PostChangeLogService postChangeLogService,
|
PostChangeLogService postChangeLogService,
|
||||||
|
PointHistoryRepository pointHistoryRepository,
|
||||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
|
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
|
||||||
RedisTemplate redisTemplate) {
|
RedisTemplate redisTemplate) {
|
||||||
this.postRepository = postRepository;
|
this.postRepository = postRepository;
|
||||||
@@ -125,6 +125,7 @@ public class PostService {
|
|||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.pointService = pointService;
|
this.pointService = pointService;
|
||||||
this.postChangeLogService = postChangeLogService;
|
this.postChangeLogService = postChangeLogService;
|
||||||
|
this.pointHistoryRepository = pointHistoryRepository;
|
||||||
this.publishMode = publishMode;
|
this.publishMode = publishMode;
|
||||||
|
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
@@ -190,7 +191,7 @@ public class PostService {
|
|||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
@CacheEvict(
|
@CacheEvict(
|
||||||
value = CachingConfig.POST_CACHE_NAME, allEntries = true
|
value = CachingConfig.POST_CACHE_NAME, allEntries = true
|
||||||
)
|
)
|
||||||
public Post createPost(String username,
|
public Post createPost(String username,
|
||||||
Long categoryId,
|
Long categoryId,
|
||||||
@@ -861,6 +862,25 @@ public class PostService {
|
|||||||
notificationRepository.deleteAll(notificationRepository.findByPost(post));
|
notificationRepository.deleteAll(notificationRepository.findByPost(post));
|
||||||
postReadService.deleteByPost(post);
|
postReadService.deleteByPost(post);
|
||||||
imageUploader.removeReferences(imageUploader.extractUrls(post.getContent()));
|
imageUploader.removeReferences(imageUploader.extractUrls(post.getContent()));
|
||||||
|
List<PointHistory> pointHistories = pointHistoryRepository.findByPost(post);
|
||||||
|
Set<User> usersToRecalculate = pointHistories.stream()
|
||||||
|
.map(PointHistory::getUser)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!pointHistories.isEmpty()) {
|
||||||
|
LocalDateTime deletedAt = LocalDateTime.now();
|
||||||
|
for (PointHistory history : pointHistories) {
|
||||||
|
history.setDeletedAt(deletedAt);
|
||||||
|
history.setPost(null);
|
||||||
|
}
|
||||||
|
pointHistoryRepository.saveAll(pointHistories);
|
||||||
|
}
|
||||||
|
if (!usersToRecalculate.isEmpty()) {
|
||||||
|
for (User affected : usersToRecalculate) {
|
||||||
|
int newPoints = pointService.recalculateUserPoints(affected);
|
||||||
|
affected.setPoint(newPoints);
|
||||||
|
}
|
||||||
|
userRepository.saveAll(usersToRecalculate);
|
||||||
|
}
|
||||||
if (post instanceof LotteryPost lp) {
|
if (post instanceof LotteryPost lp) {
|
||||||
ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId());
|
ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId());
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
@@ -868,6 +888,7 @@ public class PostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String title = post.getTitle();
|
String title = post.getTitle();
|
||||||
|
postChangeLogService.deleteLogsForPost(post);
|
||||||
postRepository.delete(post);
|
postRepository.delete(post);
|
||||||
if (adminDeleting) {
|
if (adminDeleting) {
|
||||||
notificationService.createNotification(author, NotificationType.POST_DELETED,
|
notificationService.createNotification(author, NotificationType.POST_DELETED,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public class UserService {
|
|||||||
* @param user
|
* @param user
|
||||||
*/
|
*/
|
||||||
public void sendVerifyMail(User user, VerifyType verifyType){
|
public void sendVerifyMail(User user, VerifyType verifyType){
|
||||||
//缓存验证码
|
// 缓存验证码
|
||||||
String code = genCode();
|
String code = genCode();
|
||||||
String key;
|
String key;
|
||||||
String subject;
|
String subject;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Add comment count and last reply time fields to posts table for performance optimization
|
||||||
|
ALTER TABLE posts ADD COLUMN comment_count BIGINT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE posts ADD COLUMN last_reply_at DATETIME(6) NULL;
|
||||||
|
|
||||||
|
-- Add index on last_reply_at for sorting by latest reply
|
||||||
|
CREATE INDEX idx_posts_last_reply_at ON posts(last_reply_at);
|
||||||
|
|
||||||
|
-- Initialize comment_count and last_reply_at with existing data
|
||||||
|
UPDATE posts p SET
|
||||||
|
comment_count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM comments c
|
||||||
|
WHERE c.post_id = p.id AND c.deleted_at IS NULL
|
||||||
|
),
|
||||||
|
last_reply_at = (
|
||||||
|
SELECT MAX(c.created_at)
|
||||||
|
FROM comments c
|
||||||
|
WHERE c.post_id = p.id AND c.deleted_at IS NULL
|
||||||
|
);
|
||||||
@@ -55,6 +55,10 @@ class PostControllerTest {
|
|||||||
private UserVisitService userVisitService;
|
private UserVisitService userVisitService;
|
||||||
@MockBean
|
@MockBean
|
||||||
private PostReadService postReadService;
|
private PostReadService postReadService;
|
||||||
|
@MockBean
|
||||||
|
private MedalService medalService;
|
||||||
|
@MockBean
|
||||||
|
private com.openisle.repository.PollVoteRepository pollVoteRepository;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createAndGetPost() throws Exception {
|
void createAndGetPost() throws Exception {
|
||||||
@@ -63,9 +67,13 @@ class PostControllerTest {
|
|||||||
Category cat = new Category();
|
Category cat = new Category();
|
||||||
cat.setId(1L);
|
cat.setId(1L);
|
||||||
cat.setName("tech");
|
cat.setName("tech");
|
||||||
|
cat.setDescription("Technology category");
|
||||||
|
cat.setIcon("tech-icon");
|
||||||
Tag tag = new Tag();
|
Tag tag = new Tag();
|
||||||
tag.setId(1L);
|
tag.setId(1L);
|
||||||
tag.setName("java");
|
tag.setName("java");
|
||||||
|
tag.setDescription("Java programming language");
|
||||||
|
tag.setIcon("java-icon");
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
post.setId(1L);
|
post.setId(1L);
|
||||||
post.setTitle("t");
|
post.setTitle("t");
|
||||||
@@ -111,9 +119,13 @@ class PostControllerTest {
|
|||||||
Category cat = new Category();
|
Category cat = new Category();
|
||||||
cat.setId(1L);
|
cat.setId(1L);
|
||||||
cat.setName("tech");
|
cat.setName("tech");
|
||||||
|
cat.setDescription("Technology category");
|
||||||
|
cat.setIcon("tech-icon");
|
||||||
Tag tag = new Tag();
|
Tag tag = new Tag();
|
||||||
tag.setId(1L);
|
tag.setId(1L);
|
||||||
tag.setName("java");
|
tag.setName("java");
|
||||||
|
tag.setDescription("Java programming language");
|
||||||
|
tag.setIcon("java-icon");
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
post.setId(1L);
|
post.setId(1L);
|
||||||
post.setTitle("t2");
|
post.setTitle("t2");
|
||||||
@@ -147,9 +159,13 @@ class PostControllerTest {
|
|||||||
Category cat = new Category();
|
Category cat = new Category();
|
||||||
cat.setId(1L);
|
cat.setId(1L);
|
||||||
cat.setName("tech");
|
cat.setName("tech");
|
||||||
|
cat.setDescription("Technology category");
|
||||||
|
cat.setIcon("tech-icon");
|
||||||
Tag tag = new Tag();
|
Tag tag = new Tag();
|
||||||
tag.setId(1L);
|
tag.setId(1L);
|
||||||
tag.setName("java");
|
tag.setName("java");
|
||||||
|
tag.setDescription("Java programming language");
|
||||||
|
tag.setIcon("java-icon");
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
post.setId(2L);
|
post.setId(2L);
|
||||||
post.setTitle("hello");
|
post.setTitle("hello");
|
||||||
@@ -197,9 +213,13 @@ class PostControllerTest {
|
|||||||
Category cat = new Category();
|
Category cat = new Category();
|
||||||
cat.setId(1L);
|
cat.setId(1L);
|
||||||
cat.setName("tech");
|
cat.setName("tech");
|
||||||
|
cat.setDescription("Technology category");
|
||||||
|
cat.setIcon("tech-icon");
|
||||||
Tag tag = new Tag();
|
Tag tag = new Tag();
|
||||||
tag.setId(1L);
|
tag.setId(1L);
|
||||||
tag.setName("java");
|
tag.setName("java");
|
||||||
|
tag.setDescription("Java programming language");
|
||||||
|
tag.setIcon("java-icon");
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
post.setId(1L);
|
post.setId(1L);
|
||||||
post.setTitle("t");
|
post.setTitle("t");
|
||||||
@@ -262,6 +282,8 @@ class PostControllerTest {
|
|||||||
Category cat = new Category();
|
Category cat = new Category();
|
||||||
cat.setId(1L);
|
cat.setId(1L);
|
||||||
cat.setName("tech");
|
cat.setName("tech");
|
||||||
|
cat.setDescription("Technology category");
|
||||||
|
cat.setIcon("tech-icon");
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
post.setId(1L);
|
post.setId(1L);
|
||||||
post.setTitle("t");
|
post.setTitle("t");
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.openisle.service;
|
||||||
|
|
||||||
|
import com.openisle.model.*;
|
||||||
|
import com.openisle.repository.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@TestPropertySource(locations = "classpath:application.properties")
|
||||||
|
@Transactional
|
||||||
|
public class PostCommentStatsTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostRepository postRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CategoryRepository categoryRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TagRepository tagRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CommentService commentService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostCommentStatsUpdate() {
|
||||||
|
// Create test user
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername("testuser");
|
||||||
|
user.setEmail("test@example.com");
|
||||||
|
user.setPassword("hash");
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
|
// Create test category
|
||||||
|
Category category = new Category();
|
||||||
|
category.setName("Test Category");
|
||||||
|
category.setDescription("Test Category Description");
|
||||||
|
category.setIcon("test-icon");
|
||||||
|
category = categoryRepository.save(category);
|
||||||
|
|
||||||
|
// Create test tag
|
||||||
|
Tag tag = new Tag();
|
||||||
|
tag.setName("Test Tag");
|
||||||
|
tag.setDescription("Test Tag Description");
|
||||||
|
tag.setIcon("test-tag-icon");
|
||||||
|
tag = tagRepository.save(tag);
|
||||||
|
|
||||||
|
// Create test post
|
||||||
|
Post post = new Post();
|
||||||
|
post.setTitle("Test Post");
|
||||||
|
post.setContent("Test content");
|
||||||
|
post.setAuthor(user);
|
||||||
|
post.setCategory(category);
|
||||||
|
post.getTags().add(tag);
|
||||||
|
post.setStatus(PostStatus.PUBLISHED);
|
||||||
|
post.setCommentCount(0L);
|
||||||
|
post = postRepository.save(post);
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
assertEquals(0L, post.getCommentCount());
|
||||||
|
assertNull(post.getLastReplyAt());
|
||||||
|
|
||||||
|
// Add a comment
|
||||||
|
commentService.addComment("testuser", post.getId(), "Test comment");
|
||||||
|
|
||||||
|
// Refresh post from database
|
||||||
|
post = postRepository.findById(post.getId()).orElseThrow();
|
||||||
|
|
||||||
|
// Verify comment count and last reply time are updated
|
||||||
|
assertEquals(1L, post.getCommentCount());
|
||||||
|
assertNotNull(post.getLastReplyAt());
|
||||||
|
|
||||||
|
// Add another comment
|
||||||
|
commentService.addComment("testuser", post.getId(), "Another comment");
|
||||||
|
|
||||||
|
// Refresh post again
|
||||||
|
post = postRepository.findById(post.getId()).orElseThrow();
|
||||||
|
|
||||||
|
// Verify comment count is updated
|
||||||
|
assertEquals(2L, post.getCommentCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,11 @@ import org.springframework.data.redis.core.RedisTemplate;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@@ -39,12 +42,14 @@ class PostServiceTest {
|
|||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
|
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService,
|
||||||
|
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -60,11 +65,13 @@ class PostServiceTest {
|
|||||||
when(reactionRepo.findByPost(post)).thenReturn(List.of());
|
when(reactionRepo.findByPost(post)).thenReturn(List.of());
|
||||||
when(subRepo.findByPost(post)).thenReturn(List.of());
|
when(subRepo.findByPost(post)).thenReturn(List.of());
|
||||||
when(notificationRepo.findByPost(post)).thenReturn(List.of());
|
when(notificationRepo.findByPost(post)).thenReturn(List.of());
|
||||||
|
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
|
||||||
|
|
||||||
service.deletePost(1L, "alice");
|
service.deletePost(1L, "alice");
|
||||||
|
|
||||||
verify(postReadService).deleteByPost(post);
|
verify(postReadService).deleteByPost(post);
|
||||||
verify(postRepo).delete(post);
|
verify(postRepo).delete(post);
|
||||||
|
verify(postChangeLogService).deleteLogsForPost(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -90,12 +97,14 @@ class PostServiceTest {
|
|||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
|
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService,
|
||||||
|
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -117,6 +126,7 @@ class PostServiceTest {
|
|||||||
when(reactionRepo.findByPost(post)).thenReturn(List.of());
|
when(reactionRepo.findByPost(post)).thenReturn(List.of());
|
||||||
when(subRepo.findByPost(post)).thenReturn(List.of());
|
when(subRepo.findByPost(post)).thenReturn(List.of());
|
||||||
when(notificationRepo.findByPost(post)).thenReturn(List.of());
|
when(notificationRepo.findByPost(post)).thenReturn(List.of());
|
||||||
|
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
|
||||||
|
|
||||||
service.deletePost(1L, "admin");
|
service.deletePost(1L, "admin");
|
||||||
|
|
||||||
@@ -147,12 +157,14 @@ class PostServiceTest {
|
|||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
|
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService,
|
||||||
|
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
||||||
@@ -162,6 +174,77 @@ class PostServiceTest {
|
|||||||
null, null, null, null, null, null, null, null, null));
|
null, null, null, null, null, null, null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deletePostRemovesPointHistoriesAndRecalculatesPoints() {
|
||||||
|
PostRepository postRepo = mock(PostRepository.class);
|
||||||
|
UserRepository userRepo = mock(UserRepository.class);
|
||||||
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
|
CommentService commentService = mock(CommentService.class);
|
||||||
|
CommentRepository commentRepo = mock(CommentRepository.class);
|
||||||
|
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
||||||
|
PostSubscriptionRepository subRepo = mock(PostSubscriptionRepository.class);
|
||||||
|
NotificationRepository notificationRepo = mock(NotificationRepository.class);
|
||||||
|
PostReadService postReadService = mock(PostReadService.class);
|
||||||
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
||||||
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
|
PointService pointService = mock(PointService.class);
|
||||||
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
|
||||||
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
|
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService,
|
||||||
|
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
|
||||||
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
|
Post post = new Post();
|
||||||
|
post.setId(10L);
|
||||||
|
User author = new User();
|
||||||
|
author.setId(20L);
|
||||||
|
author.setRole(Role.USER);
|
||||||
|
post.setAuthor(author);
|
||||||
|
|
||||||
|
User historyUser = new User();
|
||||||
|
historyUser.setId(30L);
|
||||||
|
|
||||||
|
PointHistory history = new PointHistory();
|
||||||
|
history.setUser(historyUser);
|
||||||
|
history.setPost(post);
|
||||||
|
|
||||||
|
when(postRepo.findById(10L)).thenReturn(Optional.of(post));
|
||||||
|
when(userRepo.findByUsername("author")).thenReturn(Optional.of(author));
|
||||||
|
when(commentRepo.findByPostAndParentIsNullOrderByCreatedAtAsc(post)).thenReturn(List.of());
|
||||||
|
when(reactionRepo.findByPost(post)).thenReturn(List.of());
|
||||||
|
when(subRepo.findByPost(post)).thenReturn(List.of());
|
||||||
|
when(notificationRepo.findByPost(post)).thenReturn(List.of());
|
||||||
|
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of(history));
|
||||||
|
when(pointService.recalculateUserPoints(historyUser)).thenReturn(0);
|
||||||
|
|
||||||
|
service.deletePost(10L, "author");
|
||||||
|
|
||||||
|
ArgumentCaptor<List<PointHistory>> captor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(pointHistoryRepository).saveAll(captor.capture());
|
||||||
|
List<PointHistory> savedHistories = captor.getValue();
|
||||||
|
assertEquals(1, savedHistories.size());
|
||||||
|
PointHistory savedHistory = savedHistories.get(0);
|
||||||
|
assertNull(savedHistory.getPost());
|
||||||
|
assertNotNull(savedHistory.getDeletedAt());
|
||||||
|
assertTrue(savedHistory.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
|
||||||
|
|
||||||
|
verify(pointService).recalculateUserPoints(historyUser);
|
||||||
|
verify(userRepo).saveAll(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void finalizeLotteryNotifiesAuthor() {
|
void finalizeLotteryNotifiesAuthor() {
|
||||||
PostRepository postRepo = mock(PostRepository.class);
|
PostRepository postRepo = mock(PostRepository.class);
|
||||||
@@ -185,12 +268,14 @@ class PostServiceTest {
|
|||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
||||||
|
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
|
||||||
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
RedisTemplate redisTemplate = mock(RedisTemplate.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate);
|
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService,
|
||||||
|
pointHistoryRepository, PublishMode.DIRECT, redisTemplate);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
User author = new User();
|
User author = new User();
|
||||||
|
|||||||
@@ -4,7 +4,18 @@ spring.datasource.username=sa
|
|||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
spring.jpa.hibernate.ddl-auto=create-drop
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
|
|
||||||
|
springdoc.info.title=openisle
|
||||||
|
springdoc.info.description=Test API documentation
|
||||||
|
springdoc.info.version=1.0.0
|
||||||
|
springdoc.info.scheme=Bearer
|
||||||
|
springdoc.info.header=Authorization
|
||||||
|
|
||||||
|
|
||||||
|
rabbitmq.queue.durable=true
|
||||||
|
rabbitmq.sharding.enabled=true
|
||||||
|
|
||||||
resend.api.key=dummy
|
resend.api.key=dummy
|
||||||
|
resend.from.email=dummy@example.com
|
||||||
cos.base-url=http://test.example.com
|
cos.base-url=http://test.example.com
|
||||||
cos.secret-id=dummy
|
cos.secret-id=dummy
|
||||||
cos.secret-key=dummy
|
cos.secret-key=dummy
|
||||||
@@ -18,6 +29,7 @@ app.upload.max-size=1048576
|
|||||||
app.jwt.secret=TestSecret
|
app.jwt.secret=TestSecret
|
||||||
app.jwt.reason-secret=TestReasonSecret
|
app.jwt.reason-secret=TestReasonSecret
|
||||||
app.jwt.reset-secret=TestResetSecret
|
app.jwt.reset-secret=TestResetSecret
|
||||||
|
app.jwt.invite-secret=TestInviteSecret
|
||||||
app.jwt.expiration=3600000
|
app.jwt.expiration=3600000
|
||||||
|
|
||||||
# Default publish mode for tests
|
# Default publish mode for tests
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
--primary-color-hover: rgb(9, 95, 105);
|
--primary-color-hover: rgb(9, 95, 105);
|
||||||
--primary-color: rgb(10, 110, 120);
|
--primary-color: rgb(10, 110, 120);
|
||||||
--primary-color-disabled: rgba(93, 152, 156, 0.5);
|
--primary-color-disabled: rgba(93, 152, 156, 0.5);
|
||||||
--secondary-color:rgb(255, 255, 255);
|
--secondary-color: rgb(255, 255, 255);
|
||||||
--secondary-color-hover:rgba(165, 255, 255, 0.447);
|
--secondary-color-hover: rgba(10, 111, 120, 0.184);
|
||||||
--new-post-icon-color: rgba(10, 111, 120, 0.598);
|
--new-post-icon-color: rgba(10, 111, 120, 0.598);
|
||||||
--header-height: 60px;
|
--header-height: 60px;
|
||||||
--header-background-color: white;
|
--header-background-color: white;
|
||||||
|
|||||||
@@ -712,13 +712,13 @@ watch(selectedTab, async (val) => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
background-color: var(--secondary-color);
|
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-page-header-unsubscribe-button:hover,
|
||||||
.profile-page-header-send-mail-button:hover {
|
.profile-page-header-send-mail-button:hover {
|
||||||
background-color: var(--secondary-color-hover);
|
background-color: var(--secondary-color-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user