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