mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-02 01:50:46 +08:00
Add post change log tracking
This commit is contained in:
@@ -37,22 +37,22 @@ public class AdminPostController {
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/pin")
|
||||
public PostSummaryDto pin(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.pinPost(id));
|
||||
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
||||
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/unpin")
|
||||
public PostSummaryDto unpin(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.unpinPost(id));
|
||||
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
||||
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/rss-exclude")
|
||||
public PostSummaryDto excludeFromRss(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.excludeFromRss(id));
|
||||
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
||||
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/rss-include")
|
||||
public PostSummaryDto includeInRss(@PathVariable Long id) {
|
||||
return postMapper.toSummaryDto(postService.includeInRss(id));
|
||||
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
||||
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.dto.PostChangeLogDto;
|
||||
import com.openisle.mapper.PostChangeLogMapper;
|
||||
import com.openisle.service.PostChangeLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/posts")
|
||||
@RequiredArgsConstructor
|
||||
public class PostChangeLogController {
|
||||
private final PostChangeLogService changeLogService;
|
||||
private final PostChangeLogMapper mapper;
|
||||
|
||||
@GetMapping("/{id}/change-logs")
|
||||
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
||||
return changeLogService.listLogs(id).stream()
|
||||
.map(mapper::toDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
30
backend/src/main/java/com/openisle/dto/PostChangeLogDto.java
Normal file
30
backend/src/main/java/com/openisle/dto/PostChangeLogDto.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import com.openisle.model.PostChangeType;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class PostChangeLogDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private PostChangeType type;
|
||||
private LocalDateTime time;
|
||||
private String oldTitle;
|
||||
private String newTitle;
|
||||
private String oldContent;
|
||||
private String newContent;
|
||||
private String oldCategory;
|
||||
private String newCategory;
|
||||
private String oldTags;
|
||||
private String newTags;
|
||||
private Boolean oldClosed;
|
||||
private Boolean newClosed;
|
||||
private LocalDateTime oldPinnedAt;
|
||||
private LocalDateTime newPinnedAt;
|
||||
private Boolean oldFeatured;
|
||||
private Boolean newFeatured;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.openisle.mapper;
|
||||
|
||||
import com.openisle.dto.PostChangeLogDto;
|
||||
import com.openisle.model.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PostChangeLogMapper {
|
||||
public PostChangeLogDto toDto(PostChangeLog log) {
|
||||
PostChangeLogDto dto = new PostChangeLogDto();
|
||||
dto.setId(log.getId());
|
||||
dto.setUsername(log.getUser().getUsername());
|
||||
dto.setType(log.getType());
|
||||
dto.setTime(log.getCreatedAt());
|
||||
if (log instanceof PostTitleChangeLog t) {
|
||||
dto.setOldTitle(t.getOldTitle());
|
||||
dto.setNewTitle(t.getNewTitle());
|
||||
} else if (log instanceof PostContentChangeLog c) {
|
||||
dto.setOldContent(c.getOldContent());
|
||||
dto.setNewContent(c.getNewContent());
|
||||
} else if (log instanceof PostCategoryChangeLog cat) {
|
||||
dto.setOldCategory(cat.getOldCategory());
|
||||
dto.setNewCategory(cat.getNewCategory());
|
||||
} else if (log instanceof PostTagChangeLog tag) {
|
||||
dto.setOldTags(tag.getOldTags());
|
||||
dto.setNewTags(tag.getNewTags());
|
||||
} else if (log instanceof PostClosedChangeLog cl) {
|
||||
dto.setOldClosed(cl.isOldClosed());
|
||||
dto.setNewClosed(cl.isNewClosed());
|
||||
} else if (log instanceof PostPinnedChangeLog p) {
|
||||
dto.setOldPinnedAt(p.getOldPinnedAt());
|
||||
dto.setNewPinnedAt(p.getNewPinnedAt());
|
||||
} else if (log instanceof PostFeaturedChangeLog f) {
|
||||
dto.setOldFeatured(f.isOldFeatured());
|
||||
dto.setNewFeatured(f.isNewFeatured());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_category_change_logs")
|
||||
public class PostCategoryChangeLog extends PostChangeLog {
|
||||
private String oldCategory;
|
||||
private String newCategory;
|
||||
}
|
||||
37
backend/src/main/java/com/openisle/model/PostChangeLog.java
Normal file
37
backend/src/main/java/com/openisle/model/PostChangeLog.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_change_logs")
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
public abstract class PostChangeLog {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "post_id")
|
||||
private Post post;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PostChangeType type;
|
||||
}
|
||||
11
backend/src/main/java/com/openisle/model/PostChangeType.java
Normal file
11
backend/src/main/java/com/openisle/model/PostChangeType.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.openisle.model;
|
||||
|
||||
public enum PostChangeType {
|
||||
CONTENT,
|
||||
TITLE,
|
||||
CATEGORY,
|
||||
TAG,
|
||||
CLOSED,
|
||||
PINNED,
|
||||
FEATURED
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_closed_change_logs")
|
||||
public class PostClosedChangeLog extends PostChangeLog {
|
||||
private boolean oldClosed;
|
||||
private boolean newClosed;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_content_change_logs")
|
||||
public class PostContentChangeLog extends PostChangeLog {
|
||||
@Column(name = "old_content", columnDefinition = "LONGTEXT")
|
||||
private String oldContent;
|
||||
|
||||
@Column(name = "new_content", columnDefinition = "LONGTEXT")
|
||||
private String newContent;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_featured_change_logs")
|
||||
public class PostFeaturedChangeLog extends PostChangeLog {
|
||||
private boolean oldFeatured;
|
||||
private boolean newFeatured;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_pinned_change_logs")
|
||||
public class PostPinnedChangeLog extends PostChangeLog {
|
||||
private LocalDateTime oldPinnedAt;
|
||||
private LocalDateTime newPinnedAt;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_tag_change_logs")
|
||||
public class PostTagChangeLog extends PostChangeLog {
|
||||
@Column(name = "old_tags")
|
||||
private String oldTags;
|
||||
|
||||
@Column(name = "new_tags")
|
||||
private String newTags;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_title_change_logs")
|
||||
public class PostTitleChangeLog extends PostChangeLog {
|
||||
private String oldTitle;
|
||||
private String newTitle;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.PostChangeLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
|
||||
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.PostChangeLogRepository;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PostChangeLogService {
|
||||
private final PostChangeLogRepository logRepository;
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public void recordContentChange(Post post, User user, String oldContent, String newContent) {
|
||||
PostContentChangeLog log = new PostContentChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.CONTENT);
|
||||
log.setOldContent(oldContent);
|
||||
log.setNewContent(newContent);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordTitleChange(Post post, User user, String oldTitle, String newTitle) {
|
||||
PostTitleChangeLog log = new PostTitleChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.TITLE);
|
||||
log.setOldTitle(oldTitle);
|
||||
log.setNewTitle(newTitle);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordCategoryChange(Post post, User user, String oldCategory, String newCategory) {
|
||||
PostCategoryChangeLog log = new PostCategoryChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.CATEGORY);
|
||||
log.setOldCategory(oldCategory);
|
||||
log.setNewCategory(newCategory);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordTagChange(Post post, User user, Set<Tag> oldTags, Set<Tag> newTags) {
|
||||
PostTagChangeLog log = new PostTagChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.TAG);
|
||||
log.setOldTags(oldTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
|
||||
log.setNewTags(newTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordClosedChange(Post post, User user, boolean oldClosed, boolean newClosed) {
|
||||
PostClosedChangeLog log = new PostClosedChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.CLOSED);
|
||||
log.setOldClosed(oldClosed);
|
||||
log.setNewClosed(newClosed);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordPinnedChange(Post post, User user, java.time.LocalDateTime oldPinnedAt, java.time.LocalDateTime newPinnedAt) {
|
||||
PostPinnedChangeLog log = new PostPinnedChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.PINNED);
|
||||
log.setOldPinnedAt(oldPinnedAt);
|
||||
log.setNewPinnedAt(newPinnedAt);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordFeaturedChange(Post post, User user, boolean oldFeatured, boolean newFeatured) {
|
||||
PostFeaturedChangeLog log = new PostFeaturedChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(user);
|
||||
log.setType(PostChangeType.FEATURED);
|
||||
log.setOldFeatured(oldFeatured);
|
||||
log.setNewFeatured(newFeatured);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public List<PostChangeLog> listLogs(Long postId) {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
return logRepository.findByPostOrderByCreatedAtAsc(post);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import com.openisle.repository.CategoryRepository;
|
||||
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.ReactionRepository;
|
||||
import com.openisle.repository.PostSubscriptionRepository;
|
||||
@@ -74,6 +75,7 @@ public class PostService {
|
||||
private final EmailSender emailSender;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final PointService pointService;
|
||||
private final PostChangeLogService postChangeLogService;
|
||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
private String websiteUrl;
|
||||
@@ -99,6 +101,7 @@ public class PostService {
|
||||
EmailSender emailSender,
|
||||
ApplicationContext applicationContext,
|
||||
PointService pointService,
|
||||
PostChangeLogService postChangeLogService,
|
||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
||||
this.postRepository = postRepository;
|
||||
this.userRepository = userRepository;
|
||||
@@ -120,6 +123,7 @@ public class PostService {
|
||||
this.emailSender = emailSender;
|
||||
this.applicationContext = applicationContext;
|
||||
this.pointService = pointService;
|
||||
this.postChangeLogService = postChangeLogService;
|
||||
this.publishMode = publishMode;
|
||||
}
|
||||
|
||||
@@ -159,19 +163,28 @@ public class PostService {
|
||||
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
|
||||
}
|
||||
|
||||
public Post excludeFromRss(Long id) {
|
||||
public Post excludeFromRss(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"));
|
||||
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
|
||||
post.setRssExcluded(true);
|
||||
return postRepository.save(post);
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, false);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Post includeInRss(Long id) {
|
||||
public Post includeInRss(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"));
|
||||
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
|
||||
post.setRssExcluded(false);
|
||||
post = postRepository.save(post);
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
|
||||
pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
|
||||
return post;
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, true);
|
||||
notificationService.createNotification(saved.getAuthor(), NotificationType.POST_FEATURED, saved, null, null, null, null, null);
|
||||
pointService.awardForFeatured(saved.getAuthor().getUsername(), saved.getId());
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Post createPost(String username,
|
||||
@@ -638,18 +651,28 @@ public class PostService {
|
||||
return post;
|
||||
}
|
||||
|
||||
public Post pinPost(Long id) {
|
||||
public Post pinPost(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"));
|
||||
java.time.LocalDateTime oldPinned = post.getPinnedAt();
|
||||
post.setPinnedAt(java.time.LocalDateTime.now());
|
||||
return postRepository.save(post);
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordPinnedChange(saved, user, oldPinned, saved.getPinnedAt());
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Post unpinPost(Long id) {
|
||||
public Post unpinPost(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"));
|
||||
java.time.LocalDateTime oldPinned = post.getPinnedAt();
|
||||
post.setPinnedAt(null);
|
||||
return postRepository.save(post);
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordPinnedChange(saved, user, oldPinned, null);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Post closePost(Long id, String username) {
|
||||
@@ -660,8 +683,11 @@ public class PostService {
|
||||
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
boolean oldClosed = post.isClosed();
|
||||
post.setClosed(true);
|
||||
return postRepository.save(post);
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordClosedChange(saved, user, oldClosed, true);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Post reopenPost(Long id, String username) {
|
||||
@@ -672,8 +698,11 @@ public class PostService {
|
||||
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||
throw new IllegalArgumentException("Unauthorized");
|
||||
}
|
||||
boolean oldClosed = post.isClosed();
|
||||
post.setClosed(false);
|
||||
return postRepository.save(post);
|
||||
Post saved = postRepository.save(post);
|
||||
postChangeLogService.recordClosedChange(saved, user, oldClosed, false);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
@@ -702,14 +731,30 @@ public class PostService {
|
||||
if (tags.isEmpty()) {
|
||||
throw new IllegalArgumentException("Tag not found");
|
||||
}
|
||||
post.setTitle(title);
|
||||
String oldTitle = post.getTitle();
|
||||
String oldContent = post.getContent();
|
||||
Category oldCategory = post.getCategory();
|
||||
java.util.Set<com.openisle.model.Tag> oldTags = new java.util.HashSet<>(post.getTags());
|
||||
post.setTitle(title);
|
||||
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);
|
||||
if (!java.util.Objects.equals(oldTitle, title)) {
|
||||
postChangeLogService.recordTitleChange(updated, user, oldTitle, title);
|
||||
}
|
||||
if (!java.util.Objects.equals(oldContent, content)) {
|
||||
postChangeLogService.recordContentChange(updated, user, oldContent, content);
|
||||
}
|
||||
if (!java.util.Objects.equals(oldCategory.getId(), category.getId())) {
|
||||
postChangeLogService.recordCategoryChange(updated, user, oldCategory.getName(), category.getName());
|
||||
}
|
||||
java.util.Set<com.openisle.model.Tag> newTags = new java.util.HashSet<>(tags);
|
||||
if (!oldTags.equals(newTags)) {
|
||||
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
44
frontend_nuxt/components/PostChangeLogItem.vue
Normal file
44
frontend_nuxt/components/PostChangeLogItem.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div :id="`change-log-${log.id}`" class="change-log-container">
|
||||
<div class="change-log-text">
|
||||
<span class="change-log-user">{{ log.username }}</span>
|
||||
<span v-if="log.type === 'CONTENT'">变更了文章内容</span>
|
||||
<span v-else-if="log.type === 'TITLE'">变更了文章标题</span>
|
||||
<span v-else-if="log.type === 'CATEGORY'">变更了文章分类</span>
|
||||
<span v-else-if="log.type === 'TAG'">变更了文章标签</span>
|
||||
<span v-else-if="log.type === 'CLOSED'">
|
||||
<template v-if="log.newClosed">关闭了文章</template>
|
||||
<template v-else>重新打开了文章</template>
|
||||
</span>
|
||||
<span v-else-if="log.type === 'PINNED'">
|
||||
<template v-if="log.newPinnedAt">置顶了文章</template>
|
||||
<template v-else>取消置顶了文章</template>
|
||||
</span>
|
||||
<span v-else-if="log.type === 'FEATURED'">
|
||||
<template v-if="log.newFeatured">将文章设为精选</template>
|
||||
<template v-else>取消精选文章</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="change-log-time">{{ log.time }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ log: Object })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.change-log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.change-log-user {
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.change-log-time {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -122,9 +122,10 @@
|
||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
<div v-else class="comments-container">
|
||||
<BaseTimeline :items="comments">
|
||||
<BaseTimeline :items="timelineItems">
|
||||
<template #item="{ item }">
|
||||
<CommentItem
|
||||
v-if="item.kind === 'comment'"
|
||||
:key="item.id"
|
||||
:comment="item"
|
||||
:level="0"
|
||||
@@ -133,6 +134,7 @@
|
||||
:post-closed="closed"
|
||||
@deleted="onCommentDeleted"
|
||||
/>
|
||||
<PostChangeLogItem v-else :key="item.id" :log="item" />
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
</div>
|
||||
@@ -182,6 +184,7 @@ import { useRoute } from 'vue-router'
|
||||
import CommentItem from '~/components/CommentItem.vue'
|
||||
import CommentEditor from '~/components/CommentEditor.vue'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import PostChangeLogItem from '~/components/PostChangeLogItem.vue'
|
||||
import ArticleTags from '~/components/ArticleTags.vue'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||
@@ -212,6 +215,7 @@ const category = ref('')
|
||||
const tags = ref([])
|
||||
const postReactions = ref([])
|
||||
const comments = ref([])
|
||||
const changeLogs = ref([])
|
||||
const status = ref('PUBLISHED')
|
||||
const closed = ref(false)
|
||||
const pinnedAt = ref(null)
|
||||
@@ -225,6 +229,11 @@ const subscribed = ref(false)
|
||||
const commentSort = ref('NEWEST')
|
||||
const isFetchingComments = ref(false)
|
||||
const isMobile = useIsMobile()
|
||||
const timelineItems = computed(() => {
|
||||
const cs = comments.value.map((c) => ({ ...c, kind: 'comment' }))
|
||||
const ls = changeLogs.value.map((l) => ({ ...l, kind: 'log' }))
|
||||
return [...cs, ...ls].sort((a, b) => new Date(a.time) - new Date(b.time))
|
||||
})
|
||||
|
||||
const headerHeight = import.meta.client
|
||||
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
||||
@@ -290,8 +299,13 @@ const gatherPostItems = () => {
|
||||
const main = mainContainer.value.querySelector('.info-content-container')
|
||||
if (main) items.push({ el: main, top: getTop(main) })
|
||||
|
||||
for (const c of comments.value) {
|
||||
const el = document.getElementById('comment-' + c.id)
|
||||
for (const c of timelineItems.value) {
|
||||
let el
|
||||
if (c.kind === 'comment') {
|
||||
el = document.getElementById('comment-' + c.id)
|
||||
} else {
|
||||
el = document.getElementById('change-log-' + c.id)
|
||||
}
|
||||
if (el) {
|
||||
items.push({ el, top: getTop(el) })
|
||||
}
|
||||
@@ -329,6 +343,16 @@ const mapComment = (
|
||||
parentUserClick: parentUserId ? () => navigateTo(`/users/${parentUserId}`) : null,
|
||||
})
|
||||
|
||||
const mapChangeLog = (l) => ({
|
||||
id: l.id,
|
||||
username: l.username,
|
||||
type: l.type,
|
||||
time: TimeManager.format(l.time),
|
||||
newClosed: l.newClosed,
|
||||
newPinnedAt: l.newPinnedAt,
|
||||
newFeatured: l.newFeatured,
|
||||
})
|
||||
|
||||
const getTop = (el) => {
|
||||
return el.getBoundingClientRect().top + window.scrollY
|
||||
}
|
||||
@@ -422,19 +446,21 @@ watchEffect(() => {
|
||||
// router.replace('/404')
|
||||
// }
|
||||
|
||||
const totalPosts = computed(() => comments.value.length + 1)
|
||||
const totalPosts = computed(() => timelineItems.value.length + 1)
|
||||
const lastReplyTime = computed(() =>
|
||||
comments.value.length ? comments.value[comments.value.length - 1].time : postTime.value,
|
||||
timelineItems.value.length
|
||||
? timelineItems.value[timelineItems.value.length - 1].time
|
||||
: postTime.value,
|
||||
)
|
||||
const firstReplyTime = computed(() =>
|
||||
comments.value.length ? comments.value[0].time : postTime.value,
|
||||
timelineItems.value.length ? timelineItems.value[0].time : postTime.value,
|
||||
)
|
||||
const scrollerTopTime = computed(() =>
|
||||
commentSort.value === 'OLDEST' ? postTime.value : firstReplyTime.value,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => comments.value.length,
|
||||
() => timelineItems.value.length,
|
||||
async () => {
|
||||
await nextTick()
|
||||
gatherPostItems()
|
||||
@@ -546,6 +572,7 @@ const approvePost = async () => {
|
||||
status.value = 'PUBLISHED'
|
||||
toast.success('已通过审核')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -561,6 +588,7 @@ const pinPost = async () => {
|
||||
if (res.ok) {
|
||||
toast.success('已置顶')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -576,6 +604,7 @@ const unpinPost = async () => {
|
||||
if (res.ok) {
|
||||
toast.success('已取消置顶')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -591,6 +620,7 @@ const excludeRss = async () => {
|
||||
if (res.ok) {
|
||||
rssExcluded.value = true
|
||||
toast.success('已标记为rss不推荐')
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -606,6 +636,7 @@ const includeRss = async () => {
|
||||
if (res.ok) {
|
||||
rssExcluded.value = false
|
||||
toast.success('已标记为rss推荐')
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -622,6 +653,7 @@ const closePost = async () => {
|
||||
closed.value = true
|
||||
toast.success('已关闭')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -638,6 +670,7 @@ const reopenPost = async () => {
|
||||
closed.value = false
|
||||
toast.success('已重新打开')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -682,6 +715,7 @@ const rejectPost = async () => {
|
||||
status.value = 'REJECTED'
|
||||
toast.success('已驳回')
|
||||
await refreshPost()
|
||||
await fetchChangeLogs()
|
||||
} else {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
@@ -740,6 +774,20 @@ const fetchComments = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchChangeLogs = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/change-logs`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
changeLogs.value = data.map(mapChangeLog)
|
||||
await nextTick()
|
||||
gatherPostItems()
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Fetch change logs error', e)
|
||||
}
|
||||
}
|
||||
|
||||
watch(commentSort, fetchComments)
|
||||
|
||||
const jumpToHashComment = async () => {
|
||||
@@ -763,7 +811,7 @@ const gotoProfile = () => {
|
||||
|
||||
const initPage = async () => {
|
||||
scrollTo(0, 0)
|
||||
await fetchComments()
|
||||
await Promise.all([fetchComments(), fetchChangeLogs()])
|
||||
const hash = location.hash
|
||||
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
|
||||
if (id) expandCommentPath(id)
|
||||
|
||||
Reference in New Issue
Block a user