diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 0a039975e..c0761c792 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -66,6 +66,7 @@ public class PostController { req.getContent(), req.getTagIds(), req.getType(), + req.getPostVisibleScopeType(), req.getPrizeDescription(), req.getPrizeIcon(), req.getPrizeCount(), @@ -101,7 +102,8 @@ public class PostController { req.getCategoryId(), req.getTitle(), req.getContent(), - req.getTagIds() + req.getTagIds(), + req.getPostVisibleScopeType() ); return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName())); } diff --git a/backend/src/main/java/com/openisle/dto/PostRequest.java b/backend/src/main/java/com/openisle/dto/PostRequest.java index 0419804ea..db24cea93 100644 --- a/backend/src/main/java/com/openisle/dto/PostRequest.java +++ b/backend/src/main/java/com/openisle/dto/PostRequest.java @@ -3,6 +3,8 @@ package com.openisle.dto; import com.openisle.model.PostType; import java.time.LocalDateTime; import java.util.List; + +import com.openisle.model.PostVisibleScopeType; import lombok.Data; /** @@ -19,6 +21,7 @@ public class PostRequest { // optional for lottery posts private PostType type; + private PostVisibleScopeType postVisibleScopeType; private String prizeDescription; private String prizeIcon; private Integer prizeCount; diff --git a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java index b2fc5c1bf..89f668ccf 100644 --- a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java +++ b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java @@ -4,6 +4,8 @@ import com.openisle.model.PostStatus; import com.openisle.model.PostType; import java.time.LocalDateTime; import java.util.List; + +import com.openisle.model.PostVisibleScopeType; import lombok.Data; /** @@ -34,4 +36,5 @@ public class PostSummaryDto { private PollDto poll; private boolean rssExcluded; private boolean closed; + private PostVisibleScopeType visibleScope; } diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index d09e0e896..b722fb704 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -73,6 +73,7 @@ public class PostMapper { dto.setPinnedAt(post.getPinnedAt()); dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded()); dto.setClosed(post.isClosed()); + dto.setVisibleScope(post.getVisibleScope()); List reactions = reactionService .getReactionsForPost(post.getId()) diff --git a/backend/src/main/java/com/openisle/model/Post.java b/backend/src/main/java/com/openisle/model/Post.java index b3ecb4a03..dbca2c843 100644 --- a/backend/src/main/java/com/openisle/model/Post.java +++ b/backend/src/main/java/com/openisle/model/Post.java @@ -66,6 +66,10 @@ public class Post { @Column(nullable = false) private PostType type = PostType.NORMAL; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PostVisibleScopeType visibleScope = PostVisibleScopeType.ALL; + @Column(nullable = false) private boolean closed = false; diff --git a/backend/src/main/java/com/openisle/model/PostVisibleScopeType.java b/backend/src/main/java/com/openisle/model/PostVisibleScopeType.java new file mode 100644 index 000000000..5c3a2e9bb --- /dev/null +++ b/backend/src/main/java/com/openisle/model/PostVisibleScopeType.java @@ -0,0 +1,32 @@ +package com.openisle.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum PostVisibleScopeType { + ALL, + ONLY_ME, + ONLY_REGISTER; + + /** + * 防止画面传递错误的值 + * @param value + * @return + */ + @JsonCreator + public static PostVisibleScopeType fromString(String value) { + if (value == null) return ALL; + for (PostVisibleScopeType type : PostVisibleScopeType.values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + // 不匹配时给默认值,而不是抛异常 + return ALL; + } + + @JsonValue + public String toValue() { + return this.name(); + } +} diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index c28566609..f5d82117a 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -1,6 +1,7 @@ package com.openisle.service; import com.openisle.config.CachingConfig; +import com.openisle.exception.NotFoundException; import com.openisle.exception.RateLimitException; import com.openisle.mapper.PostMapper; import com.openisle.model.*; @@ -225,6 +226,7 @@ public class PostService { String content, List tagIds, PostType type, + PostVisibleScopeType postVisibleScopeType, String prizeDescription, String prizeIcon, Integer prizeCount, @@ -288,6 +290,14 @@ public class PostService { post.setCategory(category); post.setTags(new HashSet<>(tags)); post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED); + + // 什么都没设置的情况下,默认为ALL + if(Objects.isNull(postVisibleScopeType)){ + post.setVisibleScope(PostVisibleScopeType.ALL); + }else{ + post.setVisibleScope(postVisibleScopeType); + } + if (post instanceof LotteryPost) { post = lotteryPostRepository.save((LotteryPost) post); } else if (post instanceof PollPost) { @@ -571,7 +581,7 @@ public class PostService { .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"); + throw new com.openisle.exception.NotFoundException("User not found"); } User viewerUser = userRepository .findByUsername(viewer) @@ -1002,7 +1012,8 @@ public class PostService { Long categoryId, String title, String content, - java.util.List tagIds + List tagIds, + PostVisibleScopeType postVisibleScopeType ) { if (tagIds == null || tagIds.isEmpty()) { throw new IllegalArgumentException("At least one tag required"); @@ -1034,6 +1045,7 @@ public class PostService { post.setContent(content); post.setCategory(category); post.setTags(new java.util.HashSet<>(tags)); + post.setVisibleScope(postVisibleScopeType); Post updated = postRepository.save(post); imageUploader.adjustReferences(oldContent, content); notificationService.notifyMentions(content, user, updated, null); diff --git a/backend/src/main/resources/db/migration/V6__add_visible_scope_to_posts.sql b/backend/src/main/resources/db/migration/V6__add_visible_scope_to_posts.sql new file mode 100644 index 000000000..2e2e89f6f --- /dev/null +++ b/backend/src/main/resources/db/migration/V6__add_visible_scope_to_posts.sql @@ -0,0 +1 @@ +ALTER TABLE posts ADD COLUMN visible_scope ENUM('ALL', 'ONLY_ME', 'ONLY_REGISTER') NOT NULL DEFAULT 'ALL' \ No newline at end of file diff --git a/frontend_nuxt/components/PostVisibleScopeSelect.vue b/frontend_nuxt/components/PostVisibleScopeSelect.vue new file mode 100644 index 000000000..b3cadd145 --- /dev/null +++ b/frontend_nuxt/components/PostVisibleScopeSelect.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend_nuxt/pages/new-post.vue b/frontend_nuxt/pages/new-post.vue index bd13de3f5..7eba0a303 100644 --- a/frontend_nuxt/pages/new-post.vue +++ b/frontend_nuxt/pages/new-post.vue @@ -11,6 +11,7 @@ +
清空
@@ -52,6 +53,7 @@ import LotteryForm from '~/components/LotteryForm.vue' import PollForm from '~/components/PollForm.vue' import { toast } from '~/main' import { authState, getToken } from '~/utils/auth' +import PostVisibleScopeSelect from '~/components/PostVisibleScopeSelect.vue' const config = useRuntimeConfig() const API_BASE_URL = config.public.apiBaseUrl @@ -60,6 +62,7 @@ const content = ref('') const selectedCategory = ref('') const selectedTags = ref([]) const postType = ref('NORMAL') +const postVisibleScope = ref('ALL') const lottery = reactive({ prizeIcon: '', prizeIconFile: null, @@ -94,6 +97,7 @@ const loadDraft = async () => { content.value = data.content || '' selectedCategory.value = data.categoryId || '' selectedTags.value = data.tagIds || [] + postVisibleScope.value = data.visiblescope toast.success('草稿已加载') } @@ -109,6 +113,7 @@ const clearPost = async () => { content.value = '' selectedCategory.value = '' selectedTags.value = [] + postVisibleScope.value = 'ALL' postType.value = 'NORMAL' lottery.prizeIcon = '' lottery.prizeIconFile = null @@ -160,6 +165,7 @@ const saveDraft = async () => { content: content.value, categoryId: selectedCategory.value || null, tagIds, + postVisibleScopeType:postVisibleScope.value }), }) if (res.ok) { @@ -314,6 +320,7 @@ const submitPost = async () => { content: content.value, categoryId: selectedCategory.value, tagIds: selectedTags.value, + postVisibleScopeType: postVisibleScope.value, type: postType.value, prizeIcon: postType.value === 'LOTTERY' ? prizeIconUrl : undefined, prizeName: postType.value === 'LOTTERY' ? lottery.prizeName : undefined, diff --git a/frontend_nuxt/pages/posts/[id]/edit.vue b/frontend_nuxt/pages/posts/[id]/edit.vue index 8ab6e51a3..e4af866e1 100644 --- a/frontend_nuxt/pages/posts/[id]/edit.vue +++ b/frontend_nuxt/pages/posts/[id]/edit.vue @@ -10,6 +10,7 @@
+
清空
@@ -44,6 +45,7 @@ import TagSelect from '~/components/TagSelect.vue' import { toast } from '~/main' import { getToken, authState } from '~/utils/auth' import LoginOverlay from '~/components/LoginOverlay.vue' +import PostVisibleScopeSelect from '~/components/PostVisibleScopeSelect.vue' const config = useRuntimeConfig() const API_BASE_URL = config.public.apiBaseUrl @@ -51,6 +53,7 @@ const title = ref('') const content = ref('') const selectedCategory = ref('') const selectedTags = ref([]) +const selectedVisibleScope = ref('ALL') const isWaitingPosting = ref(false) const isAiLoading = ref(false) const isLogin = computed(() => authState.loggedIn) @@ -70,6 +73,7 @@ const loadPost = async () => { content.value = data.content || '' selectedCategory.value = data.category.id || '' selectedTags.value = (data.tags || []).map((t) => t.id) + selectedVisibleScope.value = data.visibleScope } } catch (e) { toast.error('加载失败') @@ -180,6 +184,7 @@ const submitPost = async () => { content: content.value, categoryId: selectedCategory.value, tagIds: selectedTags.value, + postVisibleScopeType:selectedVisibleScope.value }), }) const data = await res.json() diff --git a/frontend_nuxt/pages/posts/[id]/index.vue b/frontend_nuxt/pages/posts/[id]/index.vue index 1bd91fd50..eee14eff1 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -3,169 +3,197 @@
-
-
-
-
{{ title }}
- -
-
-
审核中
-
已拒绝
-
精品
-
已关闭
-
- -
- {{ isMobile ? '订阅' : '订阅文章' }} -
-
-
- -
- {{ isMobile ? '退订' : '取消订阅' }} -
-
- -