diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 985c9a448..d59d4cb7b 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(), @@ -103,7 +104,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 6dce08d18..fa25f9ab9 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 11b175c59..876b27863 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -75,6 +75,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 7457c947c..edacbcd50 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.model.*; import com.openisle.repository.CategoryProposalPostRepository; @@ -252,6 +253,7 @@ public class PostService { String content, List tagIds, PostType type, + PostVisibleScopeType postVisibleScopeType, String prizeDescription, String prizeIcon, Integer prizeCount, @@ -336,6 +338,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 CategoryProposalPost categoryProposalPost) { @@ -716,7 +726,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) @@ -1147,7 +1157,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"); @@ -1179,6 +1190,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/deploy/deploy_staging.sh b/deploy/deploy_staging.sh index 2f6a3d066..fbeda44a7 100644 --- a/deploy/deploy_staging.sh +++ b/deploy/deploy_staging.sh @@ -36,7 +36,6 @@ echo "👉 Pull base images (for image-based services)..." docker compose -f "$compose_file" --env-file "$env_file" pull --ignore-pull-failures echo "👉 Build images (staging)..." -# 前端 + OpenSearch 都是自建镜像;--pull 更新其基础镜像 docker compose -f "$compose_file" --env-file "$env_file" \ build --pull \ --build-arg NUXT_ENV=staging \ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 6fd60458a..5bf093c77 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -161,8 +161,8 @@ services: condition: service_started websocket-service: condition: service_healthy - opensearch: - condition: service_healthy + # opensearch: + # condition: service_healthy command: > sh -c "apt-get update && apt-get install -y --no-install-recommends curl && mvn clean spring-boot:run -Dmaven.test.skip=true" diff --git a/frontend_nuxt/components/HeaderComponent.vue b/frontend_nuxt/components/HeaderComponent.vue index 0833e0e81..9a0cb911a 100644 --- a/frontend_nuxt/components/HeaderComponent.vue +++ b/frontend_nuxt/components/HeaderComponent.vue @@ -543,6 +543,7 @@ onMounted(async () => { .header-label { font-size: 12px; line-height: 1; + white-space: nowrap; } /* 在线人数的数字文字样式(无背景) */ diff --git a/frontend_nuxt/components/LoginOverlay.vue b/frontend_nuxt/components/LoginOverlay.vue index af508a404..7149c4fc6 100644 --- a/frontend_nuxt/components/LoginOverlay.vue +++ b/frontend_nuxt/components/LoginOverlay.vue @@ -3,15 +3,30 @@ 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/index.vue b/frontend_nuxt/pages/index.vue index 78a7160a7..ca18a3a7c 100644 --- a/frontend_nuxt/pages/index.vue +++ b/frontend_nuxt/pages/index.vue @@ -70,6 +70,7 @@ {{ article.title }} +
@@ -295,6 +296,7 @@ const { comments: p.commentCount, views: p.views, rssExcluded: p.rssExcluded || false, + isRestricted: p.visibleScope === 'ONLY_ME' || p.visibleScope === 'ONLY_REGISTER', time: TimeManager.format( selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt, ), @@ -336,6 +338,7 @@ const fetchNextPage = async () => { members: (p.participants || []).map((m) => ({ id: m.id, avatar: m.avatar })), comments: p.commentCount, views: p.views, + isRestricted: p.visibleScope === 'ONLY_ME' || p.visibleScope === 'ONLY_REGISTER', rssExcluded: p.rssExcluded || false, time: TimeManager.format( selectedTopic.value === '最新回复' ? p.lastReplyAt || p.createdAt : p.createdAt, diff --git a/frontend_nuxt/pages/new-post.vue b/frontend_nuxt/pages/new-post.vue index 5d03f4dad..cadb35f7e 100644 --- a/frontend_nuxt/pages/new-post.vue +++ b/frontend_nuxt/pages/new-post.vue @@ -11,6 +11,7 @@ +
清空
@@ -54,6 +55,7 @@ import PollForm from '~/components/PollForm.vue' import ProposalForm from '~/components/ProposalForm.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 @@ -62,6 +64,7 @@ const content = ref('') const selectedCategory = ref('') const selectedTags = ref([]) const postType = ref('NORMAL') +const postVisibleScope = ref('ALL') const lottery = reactive({ prizeIcon: '', prizeIconFile: null, @@ -100,6 +103,7 @@ const loadDraft = async () => { content.value = data.content || '' selectedCategory.value = data.categoryId || '' selectedTags.value = data.tagIds || [] + postVisibleScope.value = data.visiblescope toast.success('草稿已加载') } @@ -115,6 +119,7 @@ const clearPost = async () => { content.value = '' selectedCategory.value = '' selectedTags.value = [] + postVisibleScope.value = 'ALL' postType.value = 'NORMAL' lottery.prizeIcon = '' lottery.prizeIconFile = null @@ -168,6 +173,7 @@ const saveDraft = async () => { content: content.value, categoryId: selectedCategory.value || null, tagIds, + postVisibleScopeType:postVisibleScope.value }), }) if (res.ok) { @@ -328,6 +334,7 @@ const submitPost = async () => { categoryId: selectedCategory.value, tagIds: selectedTags.value, type: postType.value, + postVisibleScopeType: postVisibleScope.value, } if (postType.value === 'LOTTERY') { @@ -355,6 +362,7 @@ const submitPost = async () => { }, body: JSON.stringify(payload), }) + const data = await res.json() if (res.ok) { if (data.reward && data.reward > 0) { 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..31ce3bf8c 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -1,5 +1,22 @@