From 9a5a1df42032eb1b2ad86d508d725e76527b60ae Mon Sep 17 00:00:00 2001
From: Tim <135014430+nagisa77@users.noreply.github.com>
Date: Fri, 11 Jul 2025 14:04:33 +0800
Subject: [PATCH] Add post and tag review workflow
---
open-isle-cli/src/utils/auth.js | 13 +++-
open-isle-cli/src/views/MessagePageView.vue | 15 +++++
open-isle-cli/src/views/PostPageView.vue | 65 +++++++++++++++----
.../controller/AdminPostController.java | 2 +
.../controller/AdminTagController.java | 54 +++++++++++++++
.../openisle/controller/PostController.java | 2 +
.../openisle/controller/TagController.java | 15 ++++-
.../com/openisle/model/NotificationType.java | 2 +
src/main/java/com/openisle/model/Tag.java | 3 +
.../openisle/repository/TagRepository.java | 3 +
.../openisle/repository/UserRepository.java | 1 +
.../com/openisle/service/PostService.java | 16 ++++-
.../java/com/openisle/service/TagService.java | 24 +++++--
13 files changed, 192 insertions(+), 23 deletions(-)
create mode 100644 src/main/java/com/openisle/controller/AdminTagController.java
diff --git a/open-isle-cli/src/utils/auth.js b/open-isle-cli/src/utils/auth.js
index 5f2dd2c46..3e14ccc97 100644
--- a/open-isle-cli/src/utils/auth.js
+++ b/open-isle-cli/src/utils/auth.js
@@ -4,16 +4,19 @@ import { reactive } from 'vue'
const TOKEN_KEY = 'token'
const USER_ID_KEY = 'userId'
const USERNAME_KEY = 'username'
+const ROLE_KEY = 'role'
export const authState = reactive({
loggedIn: false,
userId: null,
- username: null
+ username: null,
+ role: null
})
authState.loggedIn = localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== ''
authState.userId = localStorage.getItem(USER_ID_KEY)
authState.username = localStorage.getItem(USERNAME_KEY)
+authState.role = localStorage.getItem(ROLE_KEY)
export function getToken() {
return localStorage.getItem(TOKEN_KEY)
@@ -33,6 +36,10 @@ export function clearToken() {
export function setUserInfo({ id, username }) {
authState.userId = id
authState.username = username
+ if (arguments[0] && arguments[0].role) {
+ authState.role = arguments[0].role
+ localStorage.setItem(ROLE_KEY, arguments[0].role)
+ }
if (id !== undefined && id !== null) localStorage.setItem(USER_ID_KEY, id)
if (username) localStorage.setItem(USERNAME_KEY, username)
}
@@ -40,8 +47,10 @@ export function setUserInfo({ id, username }) {
export function clearUserInfo() {
localStorage.removeItem(USER_ID_KEY)
localStorage.removeItem(USERNAME_KEY)
+ localStorage.removeItem(ROLE_KEY)
authState.userId = null
authState.username = null
+ authState.role = null
}
export async function fetchCurrentUser() {
@@ -61,7 +70,7 @@ export async function fetchCurrentUser() {
export async function loadCurrentUser() {
const user = await fetchCurrentUser()
if (user) {
- setUserInfo({ id: user.id, username: user.username })
+ setUserInfo({ id: user.id, username: user.username, role: user.role })
}
return user
}
diff --git a/open-isle-cli/src/views/MessagePageView.vue b/open-isle-cli/src/views/MessagePageView.vue
index c07fce9ec..bc87282f6 100644
--- a/open-isle-cli/src/views/MessagePageView.vue
+++ b/open-isle-cli/src/views/MessagePageView.vue
@@ -187,6 +187,7 @@ export default {
POST_VIEWED: 'fas fa-eye',
COMMENT_REPLY: 'fas fa-reply',
POST_REVIEWED: 'fas fa-check',
+ POST_REVIEW_REQUEST: 'fas fa-gavel',
POST_UPDATED: 'fas fa-comment-dots',
USER_ACTIVITY: 'fas fa-user',
FOLLOWED_POST: 'fas fa-feather-alt',
@@ -310,6 +311,18 @@ export default {
}
}
})
+ } else if (n.type === 'POST_REVIEW_REQUEST') {
+ notifications.value.push({
+ ...n,
+ src: n.fromUser ? n.fromUser.avatar : null,
+ icon: n.fromUser ? undefined : iconMap[n.type],
+ iconClick: () => {
+ if (n.post) {
+ markRead(n.id)
+ router.push(`/posts/${n.post.id}`)
+ }
+ }
+ })
} else {
notifications.value.push({
...n,
@@ -330,6 +343,8 @@ export default {
return '有人回复了你'
case 'REACTION':
return '有人点赞'
+ case 'POST_REVIEW_REQUEST':
+ return '帖子待审核'
case 'POST_REVIEWED':
return '帖子审核结果'
case 'POST_UPDATED':
diff --git a/open-isle-cli/src/views/PostPageView.vue b/open-isle-cli/src/views/PostPageView.vue
index b2ace0e9c..75ac6e4d2 100644
--- a/open-isle-cli/src/views/PostPageView.vue
+++ b/open-isle-cli/src/views/PostPageView.vue
@@ -13,11 +13,11 @@
-
- PENDING
+
+ 审核中
-
- REJECT
+
+ 已拒绝
取消订阅
-
+
@@ -132,9 +132,10 @@ export default {
const author = ref('')
const postContent = ref('')
const category = ref('')
- const tags = ref([])
- const postReactions = ref([])
- const comments = ref([])
+ const tags = ref([])
+ const postReactions = ref([])
+ const comments = ref([])
+ const status = ref('PUBLISHED')
const isWaitingFetchingPost = ref(false);
const isWaitingPostingComment = ref(false);
const postTime = ref('')
@@ -142,11 +143,12 @@ export default {
const mainContainer = ref(null)
const currentIndex = ref(1)
const subscribed = ref(false)
- const loggedIn = computed(() => authState.loggedIn)
+ const loggedIn = computed(() => authState.loggedIn)
+ const isAdmin = computed(() => authState.role === 'ADMIN')
const isAuthor = computed(() => authState.username === author.value.username)
- const reviewMenuItems = [
- { text: '通过审核', onClick: () => {} },
- { text: '驳回', color: 'red', onClick: () => {} }
+ const reviewMenuItems = [
+ { text: '通过审核', onClick: () => approvePost() },
+ { text: '驳回', color: 'red', onClick: () => rejectPost() }
]
const gatherPostItems = () => {
@@ -226,6 +228,7 @@ export default {
postReactions.value = data.reactions || []
comments.value = (data.comments || []).map(mapComment)
subscribed.value = !!data.subscribed
+ status.value = data.status
postTime.value = TimeManager.format(data.createdAt)
await nextTick()
gatherPostItems()
@@ -329,12 +332,42 @@ export default {
}
}
- const unsubscribePost = async () => {
+ const unsubscribePost = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
+ }
+
+ const approvePost = async () => {
+ const token = getToken()
+ if (!token) return
+ const res = await fetch(`${API_BASE_URL}/api/admin/posts/${postId}/approve`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` }
+ })
+ if (res.ok) {
+ status.value = 'PUBLISHED'
+ toast.success('已通过审核')
+ } else {
+ toast.error('操作失败')
}
+ }
+
+ const rejectPost = async () => {
+ const token = getToken()
+ if (!token) return
+ const res = await fetch(`${API_BASE_URL}/api/admin/posts/${postId}/reject`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` }
+ })
+ if (res.ok) {
+ status.value = 'REJECTED'
+ toast.success('已驳回')
+ } else {
+ toast.error('操作失败')
+ }
+ }
const res = await fetch(`${API_BASE_URL}/api/subscriptions/posts/${postId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
@@ -403,7 +436,11 @@ export default {
gotoProfile,
subscribed,
loggedIn,
- isAuthor
+ isAuthor,
+ status,
+ isAdmin,
+ approvePost,
+ rejectPost
}
}
}
diff --git a/src/main/java/com/openisle/controller/AdminPostController.java b/src/main/java/com/openisle/controller/AdminPostController.java
index fefb3fdab..0347d2cc3 100644
--- a/src/main/java/com/openisle/controller/AdminPostController.java
+++ b/src/main/java/com/openisle/controller/AdminPostController.java
@@ -45,6 +45,7 @@ public class AdminPostController {
dto.setAuthor(post.getAuthor().getUsername());
dto.setCategory(toCategoryDto(post.getCategory()));
dto.setViews(post.getViews());
+ dto.setStatus(post.getStatus());
return dto;
}
@@ -67,6 +68,7 @@ public class AdminPostController {
private String author;
private CategoryDto category;
private long views;
+ private com.openisle.model.PostStatus status;
}
@Data
diff --git a/src/main/java/com/openisle/controller/AdminTagController.java b/src/main/java/com/openisle/controller/AdminTagController.java
new file mode 100644
index 000000000..51ac5d2c6
--- /dev/null
+++ b/src/main/java/com/openisle/controller/AdminTagController.java
@@ -0,0 +1,54 @@
+package com.openisle.controller;
+
+import com.openisle.model.Tag;
+import com.openisle.service.TagService;
+import com.openisle.service.PostService;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/admin/tags")
+@RequiredArgsConstructor
+public class AdminTagController {
+ private final TagService tagService;
+ private final PostService postService;
+
+ @GetMapping("/pending")
+ public List pendingTags() {
+ return tagService.listPendingTags().stream()
+ .map(t -> toDto(t, postService.countPostsByTag(t.getId())))
+ .collect(Collectors.toList());
+ }
+
+ @PostMapping("/{id}/approve")
+ public TagDto approve(@PathVariable Long id) {
+ Tag tag = tagService.approveTag(id);
+ long count = postService.countPostsByTag(tag.getId());
+ return toDto(tag, count);
+ }
+
+ private TagDto toDto(Tag tag, long count) {
+ TagDto dto = new TagDto();
+ dto.setId(tag.getId());
+ dto.setName(tag.getName());
+ dto.setDescription(tag.getDescription());
+ dto.setIcon(tag.getIcon());
+ dto.setSmallIcon(tag.getSmallIcon());
+ dto.setCount(count);
+ return dto;
+ }
+
+ @Data
+ private static class TagDto {
+ private Long id;
+ private String name;
+ private String description;
+ private String icon;
+ private String smallIcon;
+ private Long count;
+ }
+}
diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java
index 84f53a1a4..683770d76 100644
--- a/src/main/java/com/openisle/controller/PostController.java
+++ b/src/main/java/com/openisle/controller/PostController.java
@@ -117,6 +117,7 @@ public class PostController {
dto.setCategory(toCategoryDto(post.getCategory()));
dto.setTags(post.getTags().stream().map(this::toTagDto).collect(Collectors.toList()));
dto.setViews(post.getViews());
+ dto.setStatus(post.getStatus());
List reactions = reactionService.getReactionsForPost(post.getId())
.stream()
@@ -232,6 +233,7 @@ public class PostController {
private CategoryDto category;
private java.util.List tags;
private long views;
+ private com.openisle.model.PostStatus status;
private List comments;
private List reactions;
private java.util.List participants;
diff --git a/src/main/java/com/openisle/controller/TagController.java b/src/main/java/com/openisle/controller/TagController.java
index db275dfa8..81a13266a 100644
--- a/src/main/java/com/openisle/controller/TagController.java
+++ b/src/main/java/com/openisle/controller/TagController.java
@@ -3,6 +3,9 @@ package com.openisle.controller;
import com.openisle.model.Tag;
import com.openisle.service.TagService;
import com.openisle.service.PostService;
+import com.openisle.repository.UserRepository;
+import com.openisle.model.PublishMode;
+import com.openisle.model.Role;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -16,10 +19,18 @@ import java.util.stream.Collectors;
public class TagController {
private final TagService tagService;
private final PostService postService;
+ private final UserRepository userRepository;
@PostMapping
- public TagDto create(@RequestBody TagRequest req) {
- Tag tag = tagService.createTag(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
+ public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
+ boolean approved = true;
+ if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
+ com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
+ if (user.getRole() != Role.ADMIN) {
+ approved = false;
+ }
+ }
+ Tag tag = tagService.createTag(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon(), approved);
long count = postService.countPostsByTag(tag.getId());
return toDto(tag, count);
}
diff --git a/src/main/java/com/openisle/model/NotificationType.java b/src/main/java/com/openisle/model/NotificationType.java
index ac528e34b..e68dfca9c 100644
--- a/src/main/java/com/openisle/model/NotificationType.java
+++ b/src/main/java/com/openisle/model/NotificationType.java
@@ -10,6 +10,8 @@ public enum NotificationType {
COMMENT_REPLY,
/** Someone reacted to your post or comment */
REACTION,
+ /** A new post is waiting for review */
+ POST_REVIEW_REQUEST,
/** Your post under review was approved or rejected */
POST_REVIEWED,
/** A subscribed post received a new comment */
diff --git a/src/main/java/com/openisle/model/Tag.java b/src/main/java/com/openisle/model/Tag.java
index b4b71a4eb..0a817c895 100644
--- a/src/main/java/com/openisle/model/Tag.java
+++ b/src/main/java/com/openisle/model/Tag.java
@@ -26,4 +26,7 @@ public class Tag {
@Column(name = "description", nullable = false)
private String description;
+
+ @Column(nullable = false)
+ private boolean approved = true;
}
diff --git a/src/main/java/com/openisle/repository/TagRepository.java b/src/main/java/com/openisle/repository/TagRepository.java
index 6d3722e2a..07fea768e 100644
--- a/src/main/java/com/openisle/repository/TagRepository.java
+++ b/src/main/java/com/openisle/repository/TagRepository.java
@@ -7,4 +7,7 @@ import java.util.List;
public interface TagRepository extends JpaRepository {
List findByNameContainingIgnoreCase(String keyword);
+ List findByApproved(boolean approved);
+ List findByApprovedTrue();
+ List findByNameContainingIgnoreCaseAndApprovedTrue(String keyword);
}
diff --git a/src/main/java/com/openisle/repository/UserRepository.java b/src/main/java/com/openisle/repository/UserRepository.java
index 91cd4b8b2..a4145abb4 100644
--- a/src/main/java/com/openisle/repository/UserRepository.java
+++ b/src/main/java/com/openisle/repository/UserRepository.java
@@ -8,4 +8,5 @@ public interface UserRepository extends JpaRepository {
Optional findByUsername(String username);
Optional findByEmail(String email);
java.util.List findByUsernameContainingIgnoreCase(String keyword);
+ java.util.List findByRole(com.openisle.model.Role role);
}
diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java
index 16f49f55e..ca555e673 100644
--- a/src/main/java/com/openisle/service/PostService.java
+++ b/src/main/java/com/openisle/service/PostService.java
@@ -80,6 +80,13 @@ public class PostService {
post.setTags(new java.util.HashSet<>(tags));
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
post = postRepository.save(post);
+ if (post.getStatus() == PostStatus.PENDING) {
+ java.util.List admins = userRepository.findByRole(com.openisle.model.Role.ADMIN);
+ for (User admin : admins) {
+ notificationService.createNotification(admin,
+ NotificationType.POST_REVIEW_REQUEST, post, null, null, author, null);
+ }
+ }
// notify followers of author
for (User u : subscriptionService.getSubscribers(author.getUsername())) {
if (!u.getId().equals(author.getId())) {
@@ -100,7 +107,14 @@ public class PostService {
Post post = postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Post not found"));
if (post.getStatus() != PostStatus.PUBLISHED) {
- throw new IllegalArgumentException("Post not found");
+ if (viewer == null) {
+ throw new IllegalArgumentException("Post not found");
+ }
+ User viewerUser = userRepository.findByUsername(viewer)
+ .orElseThrow(() -> new IllegalArgumentException("User not found"));
+ if (!viewerUser.getRole().equals(com.openisle.model.Role.ADMIN) && !viewerUser.getId().equals(post.getAuthor().getId())) {
+ throw new IllegalArgumentException("Post not found");
+ }
}
post.setViews(post.getViews() + 1);
post = postRepository.save(post);
diff --git a/src/main/java/com/openisle/service/TagService.java b/src/main/java/com/openisle/service/TagService.java
index ad0601639..f7e6fae5d 100644
--- a/src/main/java/com/openisle/service/TagService.java
+++ b/src/main/java/com/openisle/service/TagService.java
@@ -13,16 +13,21 @@ public class TagService {
private final TagRepository tagRepository;
private final TagValidator tagValidator;
- public Tag createTag(String name, String description, String icon, String smallIcon) {
+ public Tag createTag(String name, String description, String icon, String smallIcon, boolean approved) {
tagValidator.validate(name);
Tag tag = new Tag();
tag.setName(name);
tag.setDescription(description);
tag.setIcon(icon);
tag.setSmallIcon(smallIcon);
+ tag.setApproved(approved);
return tagRepository.save(tag);
}
+ public Tag createTag(String name, String description, String icon, String smallIcon) {
+ return createTag(name, description, icon, smallIcon, true);
+ }
+
public Tag updateTag(Long id, String name, String description, String icon, String smallIcon) {
Tag tag = tagRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
@@ -46,19 +51,30 @@ public class TagService {
tagRepository.deleteById(id);
}
+ public Tag approveTag(Long id) {
+ Tag tag = tagRepository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("Tag not found"));
+ tag.setApproved(true);
+ return tagRepository.save(tag);
+ }
+
+ public List listPendingTags() {
+ return tagRepository.findByApproved(false);
+ }
+
public Tag getTag(Long id) {
return tagRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Tag not found"));
}
public List listTags() {
- return tagRepository.findAll();
+ return tagRepository.findByApprovedTrue();
}
public List searchTags(String keyword) {
if (keyword == null || keyword.isBlank()) {
- return tagRepository.findAll();
+ return tagRepository.findByApprovedTrue();
}
- return tagRepository.findByNameContainingIgnoreCase(keyword);
+ return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
}
}