mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-09 08:30:55 +08:00
681 lines
31 KiB
Java
681 lines
31 KiB
Java
package com.openisle.service;
|
|
|
|
import com.openisle.model.Post;
|
|
import com.openisle.model.PostStatus;
|
|
import com.openisle.model.PostType;
|
|
import com.openisle.model.PublishMode;
|
|
import com.openisle.model.User;
|
|
import com.openisle.model.Category;
|
|
import com.openisle.model.Comment;
|
|
import com.openisle.model.NotificationType;
|
|
import com.openisle.model.LotteryPost;
|
|
import com.openisle.repository.PostRepository;
|
|
import com.openisle.repository.LotteryPostRepository;
|
|
import com.openisle.repository.UserRepository;
|
|
import com.openisle.repository.CategoryRepository;
|
|
import com.openisle.repository.TagRepository;
|
|
import com.openisle.service.SubscriptionService;
|
|
import com.openisle.service.CommentService;
|
|
import com.openisle.repository.CommentRepository;
|
|
import com.openisle.repository.ReactionRepository;
|
|
import com.openisle.repository.PostSubscriptionRepository;
|
|
import com.openisle.repository.NotificationRepository;
|
|
import com.openisle.model.Role;
|
|
import com.openisle.exception.RateLimitException;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.context.ApplicationContext;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.scheduling.TaskScheduler;
|
|
import com.openisle.service.EmailSender;
|
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneOffset;
|
|
import java.util.*;
|
|
|
|
import org.springframework.data.domain.PageRequest;
|
|
import org.springframework.data.domain.Pageable;
|
|
import org.springframework.data.domain.Sort;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.util.CollectionUtils;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
import org.springframework.context.event.EventListener;
|
|
|
|
@Slf4j
|
|
@Service
|
|
public class PostService {
|
|
private final PostRepository postRepository;
|
|
private final UserRepository userRepository;
|
|
private final CategoryRepository categoryRepository;
|
|
private final TagRepository tagRepository;
|
|
private final LotteryPostRepository lotteryPostRepository;
|
|
private PublishMode publishMode;
|
|
private final NotificationService notificationService;
|
|
private final SubscriptionService subscriptionService;
|
|
private final CommentService commentService;
|
|
private final CommentRepository commentRepository;
|
|
private final ReactionRepository reactionRepository;
|
|
private final PostSubscriptionRepository postSubscriptionRepository;
|
|
private final NotificationRepository notificationRepository;
|
|
private final PostReadService postReadService;
|
|
private final ImageUploader imageUploader;
|
|
private final TaskScheduler taskScheduler;
|
|
private final EmailSender emailSender;
|
|
private final ApplicationContext applicationContext;
|
|
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
|
@Value("${app.website-url:https://www.open-isle.com}")
|
|
private String websiteUrl;
|
|
|
|
@org.springframework.beans.factory.annotation.Autowired
|
|
public PostService(PostRepository postRepository,
|
|
UserRepository userRepository,
|
|
CategoryRepository categoryRepository,
|
|
TagRepository tagRepository,
|
|
LotteryPostRepository lotteryPostRepository,
|
|
NotificationService notificationService,
|
|
SubscriptionService subscriptionService,
|
|
CommentService commentService,
|
|
CommentRepository commentRepository,
|
|
ReactionRepository reactionRepository,
|
|
PostSubscriptionRepository postSubscriptionRepository,
|
|
NotificationRepository notificationRepository,
|
|
PostReadService postReadService,
|
|
ImageUploader imageUploader,
|
|
TaskScheduler taskScheduler,
|
|
EmailSender emailSender,
|
|
ApplicationContext applicationContext,
|
|
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
|
this.postRepository = postRepository;
|
|
this.userRepository = userRepository;
|
|
this.categoryRepository = categoryRepository;
|
|
this.tagRepository = tagRepository;
|
|
this.lotteryPostRepository = lotteryPostRepository;
|
|
this.notificationService = notificationService;
|
|
this.subscriptionService = subscriptionService;
|
|
this.commentService = commentService;
|
|
this.commentRepository = commentRepository;
|
|
this.reactionRepository = reactionRepository;
|
|
this.postSubscriptionRepository = postSubscriptionRepository;
|
|
this.notificationRepository = notificationRepository;
|
|
this.postReadService = postReadService;
|
|
this.imageUploader = imageUploader;
|
|
this.taskScheduler = taskScheduler;
|
|
this.emailSender = emailSender;
|
|
this.applicationContext = applicationContext;
|
|
this.publishMode = publishMode;
|
|
}
|
|
|
|
@EventListener(ApplicationReadyEvent.class)
|
|
public void rescheduleLotteries() {
|
|
LocalDateTime now = LocalDateTime.now();
|
|
for (LotteryPost lp : lotteryPostRepository.findByEndTimeAfterAndWinnersIsEmpty(now)) {
|
|
ScheduledFuture<?> future = taskScheduler.schedule(
|
|
() -> applicationContext.getBean(PostService.class).finalizeLottery(lp.getId()),
|
|
java.util.Date.from(lp.getEndTime().atZone(ZoneId.systemDefault()).toInstant()));
|
|
scheduledFinalizations.put(lp.getId(), future);
|
|
}
|
|
for (LotteryPost lp : lotteryPostRepository.findByEndTimeBeforeAndWinnersIsEmpty(now)) {
|
|
applicationContext.getBean(PostService.class).finalizeLottery(lp.getId());
|
|
}
|
|
}
|
|
|
|
public PublishMode getPublishMode() {
|
|
return publishMode;
|
|
}
|
|
|
|
public void setPublishMode(PublishMode publishMode) {
|
|
this.publishMode = publishMode;
|
|
}
|
|
|
|
public List<Post> listLatestRssPosts(int limit) {
|
|
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
|
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
|
|
}
|
|
|
|
public Post excludeFromRss(Long id) {
|
|
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
post.setRssExcluded(true);
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
public Post includeInRss(Long id) {
|
|
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
post.setRssExcluded(false);
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
public Post createPost(String username,
|
|
Long categoryId,
|
|
String title,
|
|
String content,
|
|
java.util.List<Long> tagIds,
|
|
PostType type,
|
|
String prizeDescription,
|
|
String prizeIcon,
|
|
Integer prizeCount,
|
|
LocalDateTime startTime,
|
|
LocalDateTime endTime) {
|
|
long recent = postRepository.countByAuthorAfter(username,
|
|
java.time.LocalDateTime.now().minusMinutes(5));
|
|
if (recent >= 1) {
|
|
throw new RateLimitException("Too many posts");
|
|
}
|
|
if (tagIds == null || tagIds.isEmpty()) {
|
|
throw new IllegalArgumentException("At least one tag required");
|
|
}
|
|
if (tagIds.size() > 2) {
|
|
throw new IllegalArgumentException("At most two tags allowed");
|
|
}
|
|
User author = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
Category category = categoryRepository.findById(categoryId)
|
|
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
throw new IllegalArgumentException("Tag not found");
|
|
}
|
|
PostType actualType = type != null ? type : PostType.NORMAL;
|
|
Post post;
|
|
if (actualType == PostType.LOTTERY) {
|
|
LotteryPost lp = new LotteryPost();
|
|
lp.setPrizeDescription(prizeDescription);
|
|
lp.setPrizeIcon(prizeIcon);
|
|
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
|
|
lp.setStartTime(startTime);
|
|
lp.setEndTime(endTime);
|
|
post = lp;
|
|
} else {
|
|
post = new Post();
|
|
}
|
|
post.setType(actualType);
|
|
post.setTitle(title);
|
|
post.setContent(content);
|
|
post.setAuthor(author);
|
|
post.setCategory(category);
|
|
post.setTags(new HashSet<>(tags));
|
|
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
|
|
if (post instanceof LotteryPost) {
|
|
post = lotteryPostRepository.save((LotteryPost) post);
|
|
} else {
|
|
post = postRepository.save(post);
|
|
}
|
|
imageUploader.addReferences(imageUploader.extractUrls(content));
|
|
if (post.getStatus() == PostStatus.PENDING) {
|
|
java.util.List<User> admins = userRepository.findByRole(com.openisle.model.Role.ADMIN);
|
|
for (User admin : admins) {
|
|
notificationService.createNotification(admin,
|
|
NotificationType.POST_REVIEW_REQUEST, post, null, null, author, null, null);
|
|
}
|
|
notificationService.createNotification(author,
|
|
NotificationType.POST_REVIEW_REQUEST, post, null, null, null, null, null);
|
|
}
|
|
// notify followers of author
|
|
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
|
|
if (!u.getId().equals(author.getId())) {
|
|
notificationService.createNotification(
|
|
u,
|
|
NotificationType.FOLLOWED_POST,
|
|
post,
|
|
null,
|
|
null,
|
|
author,
|
|
null,
|
|
null);
|
|
}
|
|
}
|
|
notificationService.notifyMentions(content, author, post, null);
|
|
|
|
if (post instanceof LotteryPost lp && lp.getEndTime() != null) {
|
|
ScheduledFuture<?> future = taskScheduler.schedule(
|
|
() -> applicationContext.getBean(PostService.class).finalizeLottery(lp.getId()),
|
|
java.util.Date.from(lp.getEndTime().atZone(ZoneId.systemDefault()).toInstant()));
|
|
scheduledFinalizations.put(lp.getId(), future);
|
|
}
|
|
return post;
|
|
}
|
|
|
|
public void joinLottery(Long postId, String username) {
|
|
LotteryPost post = lotteryPostRepository.findById(postId)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
post.getParticipants().add(user);
|
|
lotteryPostRepository.save(post);
|
|
}
|
|
|
|
@Transactional
|
|
public void finalizeLottery(Long postId) {
|
|
log.info("start to finalizeLottery for {}", postId);
|
|
scheduledFinalizations.remove(postId);
|
|
lotteryPostRepository.findById(postId).ifPresent(lp -> {
|
|
List<User> participants = new ArrayList<>(lp.getParticipants());
|
|
if (participants.isEmpty()) {
|
|
return;
|
|
}
|
|
Collections.shuffle(participants);
|
|
int winnersCount = Math.min(lp.getPrizeCount(), participants.size());
|
|
java.util.Set<User> winners = new java.util.HashSet<>(participants.subList(0, winnersCount));
|
|
log.info("winner count {}", winnersCount);
|
|
lp.setWinners(winners);
|
|
lotteryPostRepository.save(lp);
|
|
for (User w : winners) {
|
|
if (w.getEmail() != null) {
|
|
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
|
|
}
|
|
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
|
|
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
|
}
|
|
if (lp.getAuthor() != null) {
|
|
if (lp.getAuthor().getEmail() != null) {
|
|
emailSender.sendEmail(lp.getAuthor().getEmail(), "抽奖已开奖", "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖");
|
|
}
|
|
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
|
|
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
|
}
|
|
});
|
|
}
|
|
|
|
@Transactional
|
|
public Post viewPost(Long id, String viewer) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
if (post.getStatus() != PostStatus.PUBLISHED) {
|
|
if (viewer == null) {
|
|
throw new com.openisle.exception.NotFoundException("Post not found");
|
|
}
|
|
User viewerUser = userRepository.findByUsername(viewer)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
if (!viewerUser.getRole().equals(com.openisle.model.Role.ADMIN) && !viewerUser.getId().equals(post.getAuthor().getId())) {
|
|
throw new com.openisle.exception.NotFoundException("Post not found");
|
|
}
|
|
}
|
|
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) {
|
|
notificationRepository.deleteByTypeAndFromUserAndPost(NotificationType.POST_VIEWED, viewerUser, post);
|
|
notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null, viewerUser, null, null);
|
|
}
|
|
}
|
|
return post;
|
|
}
|
|
|
|
public List<Post> listPosts() {
|
|
return listPostsByCategories(null, null, null);
|
|
}
|
|
|
|
public List<Post> listPostsByViews(Integer page, Integer pageSize) {
|
|
return listPostsByViews(null, null, page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPostsByViews(java.util.List<Long> categoryIds,
|
|
java.util.List<Long> tagIds,
|
|
Integer page,
|
|
Integer pageSize) {
|
|
boolean hasCategories = categoryIds != null && !categoryIds.isEmpty();
|
|
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
|
|
|
java.util.List<Post> posts;
|
|
|
|
if (!hasCategories && !hasTags) {
|
|
posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED);
|
|
} else if (hasCategories) {
|
|
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
|
if (categories.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
if (hasTags) {
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
posts = postRepository.findByCategoriesAndAllTagsOrderByViewsDesc(
|
|
categories, tags, PostStatus.PUBLISHED, tags.size());
|
|
} else {
|
|
posts = postRepository.findByCategoryInAndStatusOrderByViewsDesc(categories, PostStatus.PUBLISHED);
|
|
}
|
|
} else {
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
posts = postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size());
|
|
}
|
|
|
|
return paginate(sortByPinnedAndViews(posts), page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPostsByLatestReply(Integer page, Integer pageSize) {
|
|
return listPostsByLatestReply(null, null, page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPostsByLatestReply(java.util.List<Long> categoryIds,
|
|
java.util.List<Long> tagIds,
|
|
Integer page,
|
|
Integer pageSize) {
|
|
boolean hasCategories = categoryIds != null && !categoryIds.isEmpty();
|
|
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
|
|
|
java.util.List<Post> posts;
|
|
|
|
if (!hasCategories && !hasTags) {
|
|
posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED);
|
|
} else if (hasCategories) {
|
|
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
|
if (categories.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
if (hasTags) {
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(
|
|
categories, tags, PostStatus.PUBLISHED, tags.size());
|
|
} else {
|
|
posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED);
|
|
}
|
|
} else {
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size());
|
|
}
|
|
|
|
return paginate(sortByPinnedAndLastReply(posts), page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPostsByCategories(java.util.List<Long> categoryIds,
|
|
Integer page,
|
|
Integer pageSize) {
|
|
if (categoryIds == null || categoryIds.isEmpty()) {
|
|
java.util.List<Post> posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED);
|
|
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
|
}
|
|
|
|
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
|
java.util.List<Post> posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc(categories, PostStatus.PUBLISHED);
|
|
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
|
}
|
|
|
|
public List<Post> getRecentPostsByUser(String username, int limit) {
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
Pageable pageable = PageRequest.of(0, limit);
|
|
return postRepository.findByAuthorAndStatusOrderByCreatedAtDesc(user, PostStatus.PUBLISHED, pageable);
|
|
}
|
|
|
|
public java.time.LocalDateTime getLastPostTime(String username) {
|
|
return postRepository.findLastPostTime(username);
|
|
}
|
|
|
|
public long getTotalViews(String username) {
|
|
Long v = postRepository.sumViews(username);
|
|
return v != null ? v : 0;
|
|
}
|
|
|
|
public List<Post> listPostsByTags(java.util.List<Long> tagIds,
|
|
Integer page,
|
|
Integer pageSize) {
|
|
if (tagIds == null || tagIds.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
|
|
java.util.List<Post> posts = postRepository.findByAllTagsOrderByCreatedAtDesc(tags, PostStatus.PUBLISHED, tags.size());
|
|
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPostsByCategoriesAndTags(java.util.List<Long> categoryIds,
|
|
java.util.List<Long> tagIds,
|
|
Integer page,
|
|
Integer pageSize) {
|
|
if (categoryIds == null || categoryIds.isEmpty() || tagIds == null || tagIds.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
|
|
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (categories.isEmpty() || tags.isEmpty()) {
|
|
return java.util.List.of();
|
|
}
|
|
|
|
java.util.List<Post> posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc(categories, tags, PostStatus.PUBLISHED, tags.size());
|
|
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
|
}
|
|
|
|
public List<Post> listPendingPosts() {
|
|
return postRepository.findByStatus(PostStatus.PENDING);
|
|
}
|
|
|
|
public Post approvePost(Long id) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
// publish all pending tags along with the post
|
|
for (com.openisle.model.Tag tag : post.getTags()) {
|
|
if (!tag.isApproved()) {
|
|
tag.setApproved(true);
|
|
tagRepository.save(tag);
|
|
}
|
|
}
|
|
post.setStatus(PostStatus.PUBLISHED);
|
|
post = postRepository.save(post);
|
|
notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, true, null, null, null);
|
|
return post;
|
|
}
|
|
|
|
public Post rejectPost(Long id) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
// remove user created tags that are only linked to this post
|
|
java.util.Set<com.openisle.model.Tag> tags = new java.util.HashSet<>(post.getTags());
|
|
for (com.openisle.model.Tag tag : tags) {
|
|
if (!tag.isApproved()) {
|
|
long count = postRepository.countDistinctByTags_Id(tag.getId());
|
|
if (count <= 1) {
|
|
post.getTags().remove(tag);
|
|
tagRepository.delete(tag);
|
|
}
|
|
}
|
|
}
|
|
post.setStatus(PostStatus.REJECTED);
|
|
post = postRepository.save(post);
|
|
notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, false, null, null, null);
|
|
return post;
|
|
}
|
|
|
|
public Post pinPost(Long id) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
post.setPinnedAt(java.time.LocalDateTime.now());
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
public Post unpinPost(Long id) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
post.setPinnedAt(null);
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
public Post closePost(Long id, String username) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
|
throw new IllegalArgumentException("Unauthorized");
|
|
}
|
|
post.setClosed(true);
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
public Post reopenPost(Long id, String username) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
|
throw new IllegalArgumentException("Unauthorized");
|
|
}
|
|
post.setClosed(false);
|
|
return postRepository.save(post);
|
|
}
|
|
|
|
@org.springframework.transaction.annotation.Transactional
|
|
public Post updatePost(Long id,
|
|
String username,
|
|
Long categoryId,
|
|
String title,
|
|
String content,
|
|
java.util.List<Long> tagIds) {
|
|
if (tagIds == null || tagIds.isEmpty()) {
|
|
throw new IllegalArgumentException("At least one tag required");
|
|
}
|
|
if (tagIds.size() > 2) {
|
|
throw new IllegalArgumentException("At most two tags allowed");
|
|
}
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
|
throw new IllegalArgumentException("Unauthorized");
|
|
}
|
|
Category category = categoryRepository.findById(categoryId)
|
|
.orElseThrow(() -> new IllegalArgumentException("Category not found"));
|
|
java.util.List<com.openisle.model.Tag> tags = tagRepository.findAllById(tagIds);
|
|
if (tags.isEmpty()) {
|
|
throw new IllegalArgumentException("Tag not found");
|
|
}
|
|
post.setTitle(title);
|
|
String oldContent = post.getContent();
|
|
post.setContent(content);
|
|
post.setCategory(category);
|
|
post.setTags(new java.util.HashSet<>(tags));
|
|
Post updated = postRepository.save(post);
|
|
imageUploader.adjustReferences(oldContent, content);
|
|
notificationService.notifyMentions(content, user, updated, null);
|
|
return updated;
|
|
}
|
|
|
|
@org.springframework.transaction.annotation.Transactional
|
|
public void deletePost(Long id, String username) {
|
|
Post post = postRepository.findById(id)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
User user = userRepository.findByUsername(username)
|
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
User author = post.getAuthor();
|
|
boolean adminDeleting = !user.getId().equals(author.getId()) && user.getRole() == Role.ADMIN;
|
|
if (!user.getId().equals(author.getId()) && user.getRole() != Role.ADMIN) {
|
|
throw new IllegalArgumentException("Unauthorized");
|
|
}
|
|
for (Comment c : commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post)) {
|
|
commentService.deleteCommentCascade(c);
|
|
}
|
|
reactionRepository.findByPost(post).forEach(reactionRepository::delete);
|
|
postSubscriptionRepository.findByPost(post).forEach(postSubscriptionRepository::delete);
|
|
notificationRepository.deleteAll(notificationRepository.findByPost(post));
|
|
postReadService.deleteByPost(post);
|
|
imageUploader.removeReferences(imageUploader.extractUrls(post.getContent()));
|
|
if (post instanceof LotteryPost lp) {
|
|
ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId());
|
|
if (future != null) {
|
|
future.cancel(false);
|
|
}
|
|
}
|
|
String title = post.getTitle();
|
|
postRepository.delete(post);
|
|
if (adminDeleting) {
|
|
notificationService.createNotification(author, NotificationType.POST_DELETED,
|
|
null, null, null, user, null, title);
|
|
}
|
|
}
|
|
|
|
public java.util.List<Post> getPostsByIds(java.util.List<Long> ids) {
|
|
return postRepository.findAllById(ids);
|
|
}
|
|
|
|
public long countPostsByCategory(Long categoryId) {
|
|
return postRepository.countByCategory_Id(categoryId);
|
|
}
|
|
|
|
public Map<Long, Long> countPostsByCategoryIds(List<Long> categoryIds) {
|
|
Map<Long, Long> result = new HashMap<>();
|
|
var dbResult = postRepository.countPostsByCategoryIds(categoryIds);
|
|
dbResult.forEach(r -> {
|
|
result.put(((Long)r[0]), ((Long)r[1]));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
public long countPostsByTag(Long tagId) {
|
|
return postRepository.countDistinctByTags_Id(tagId);
|
|
}
|
|
|
|
public Map<Long, Long> countPostsByTagIds(List<Long> tagIds) {
|
|
Map<Long, Long> result = new HashMap<>();
|
|
if (CollectionUtils.isEmpty(tagIds)) {
|
|
return result;
|
|
}
|
|
var dbResult = postRepository.countPostsByTagIds(tagIds);
|
|
dbResult.forEach(r -> {
|
|
result.put(((Long)r[0]), ((Long)r[1]));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
private java.util.List<Post> sortByPinnedAndCreated(java.util.List<Post> posts) {
|
|
return posts.stream()
|
|
.sorted(java.util.Comparator
|
|
.comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))
|
|
.thenComparing(Post::getCreatedAt, java.util.Comparator.reverseOrder()))
|
|
.toList();
|
|
}
|
|
|
|
private java.util.List<Post> sortByPinnedAndViews(java.util.List<Post> posts) {
|
|
return posts.stream()
|
|
.sorted(java.util.Comparator
|
|
.comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))
|
|
.thenComparing(Post::getViews, java.util.Comparator.reverseOrder()))
|
|
.toList();
|
|
}
|
|
|
|
private java.util.List<Post> sortByPinnedAndLastReply(java.util.List<Post> posts) {
|
|
return posts.stream()
|
|
.sorted(java.util.Comparator
|
|
.comparing(Post::getPinnedAt, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))
|
|
.thenComparing(p -> {
|
|
java.time.LocalDateTime t = commentRepository.findLastCommentTime(p);
|
|
return t != null ? t : p.getCreatedAt();
|
|
}, java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder())))
|
|
.toList();
|
|
}
|
|
|
|
private java.util.List<Post> paginate(java.util.List<Post> posts, Integer page, Integer pageSize) {
|
|
if (page == null || pageSize == null) {
|
|
return posts;
|
|
}
|
|
int from = page * pageSize;
|
|
if (from >= posts.size()) {
|
|
return java.util.List.of();
|
|
}
|
|
int to = Math.min(from + pageSize, posts.size());
|
|
return posts.subList(from, to);
|
|
}
|
|
}
|