From 2f339fdbdb5cefe2f67d19f8b292c890a5cd4ac8 Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:13:41 +0800 Subject: [PATCH] feat: enable multi-option polls --- .../openisle/controller/PostController.java | 4 +- .../main/java/com/openisle/dto/PollDto.java | 1 + .../java/com/openisle/dto/PostRequest.java | 1 + .../java/com/openisle/mapper/PostMapper.java | 1 + .../java/com/openisle/model/PollPost.java | 3 + .../java/com/openisle/model/PollVote.java | 2 +- .../com/openisle/service/PostService.java | 30 ++++--- .../controller/PostControllerTest.java | 4 +- .../com/openisle/service/PostServiceTest.java | 2 +- frontend_nuxt/components/PollForm.vue | 13 +++ frontend_nuxt/components/PostPoll.vue | 85 +++++++++++++++---- frontend_nuxt/pages/new-post.vue | 3 + 12 files changed, 117 insertions(+), 32 deletions(-) diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 92981bb3f..598bc7ed2 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -44,7 +44,7 @@ public class PostController { req.getType(), req.getPrizeDescription(), req.getPrizeIcon(), req.getPrizeCount(), req.getPointCost(), req.getStartTime(), req.getEndTime(), - req.getOptions()); + req.getOptions(), req.getMultiple()); draftService.deleteDraft(auth.getName()); PostDetailDto dto = postMapper.toDetailDto(post, auth.getName()); dto.setReward(levelService.awardForPost(auth.getName())); @@ -94,7 +94,7 @@ public class PostController { } @PostMapping("/{id}/poll/vote") - public ResponseEntity vote(@PathVariable Long id, @RequestParam("option") int option, Authentication auth) { + public ResponseEntity vote(@PathVariable Long id, @RequestParam("option") List option, Authentication auth) { postService.votePoll(id, auth.getName(), option); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/openisle/dto/PollDto.java b/backend/src/main/java/com/openisle/dto/PollDto.java index 889b7a5c9..af8da1de0 100644 --- a/backend/src/main/java/com/openisle/dto/PollDto.java +++ b/backend/src/main/java/com/openisle/dto/PollDto.java @@ -13,4 +13,5 @@ public class PollDto { private LocalDateTime endTime; private List participants; private Map> optionParticipants; + private boolean multiple; } diff --git a/backend/src/main/java/com/openisle/dto/PostRequest.java b/backend/src/main/java/com/openisle/dto/PostRequest.java index cd48888fc..bdebadb6c 100644 --- a/backend/src/main/java/com/openisle/dto/PostRequest.java +++ b/backend/src/main/java/com/openisle/dto/PostRequest.java @@ -28,5 +28,6 @@ public class PostRequest { private LocalDateTime endTime; // fields for poll posts private List options; + private Boolean multiple; } diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index 8c8bd223b..57277357d 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -111,6 +111,7 @@ public class PostMapper { .collect(Collectors.groupingBy(PollVote::getOptionIndex, Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList()))); p.setOptionParticipants(optionParticipants); + p.setMultiple(pp.isMultiple()); dto.setPoll(p); } } diff --git a/backend/src/main/java/com/openisle/model/PollPost.java b/backend/src/main/java/com/openisle/model/PollPost.java index c9a5363f1..503b6bfbf 100644 --- a/backend/src/main/java/com/openisle/model/PollPost.java +++ b/backend/src/main/java/com/openisle/model/PollPost.java @@ -32,6 +32,9 @@ public class PollPost extends Post { inverseJoinColumns = @JoinColumn(name = "user_id")) private Set participants = new HashSet<>(); + @Column + private boolean multiple = false; + @Column private LocalDateTime endTime; diff --git a/backend/src/main/java/com/openisle/model/PollVote.java b/backend/src/main/java/com/openisle/model/PollVote.java index 319ef975a..41994dc90 100644 --- a/backend/src/main/java/com/openisle/model/PollVote.java +++ b/backend/src/main/java/com/openisle/model/PollVote.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; @Entity -@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id"})) +@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id", "option_index"})) @Getter @Setter @NoArgsConstructor diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index 76873f858..3b088817c 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -186,7 +186,8 @@ public class PostService { Integer pointCost, LocalDateTime startTime, LocalDateTime endTime, - java.util.List options) { + java.util.List options, + Boolean multiple) { long recent = postRepository.countByAuthorAfter(username, java.time.LocalDateTime.now().minusMinutes(5)); if (recent >= 1) { @@ -227,6 +228,7 @@ public class PostService { PollPost pp = new PollPost(); pp.setOptions(options); pp.setEndTime(endTime); + pp.setMultiple(multiple != null && multiple); post = pp; } else { post = new Post(); @@ -302,7 +304,7 @@ public class PostService { } @Transactional - public PollPost votePoll(Long postId, String username, int optionIndex) { + public PollPost votePoll(Long postId, String username, java.util.List optionIndices) { PollPost post = pollPostRepository.findById(postId) .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) { @@ -313,16 +315,24 @@ public class PostService { if (post.getParticipants().contains(user)) { throw new IllegalArgumentException("User already voted"); } - if (optionIndex < 0 || optionIndex >= post.getOptions().size()) { - throw new IllegalArgumentException("Invalid option"); + if (optionIndices == null || optionIndices.isEmpty()) { + throw new IllegalArgumentException("No options selected"); + } + java.util.Set unique = new java.util.HashSet<>(optionIndices); + for (int optionIndex : unique) { + if (optionIndex < 0 || optionIndex >= post.getOptions().size()) { + throw new IllegalArgumentException("Invalid option"); + } } post.getParticipants().add(user); - post.getVotes().merge(optionIndex, 1, Integer::sum); - PollVote vote = new PollVote(); - vote.setPost(post); - vote.setUser(user); - vote.setOptionIndex(optionIndex); - pollVoteRepository.save(vote); + for (int optionIndex : unique) { + post.getVotes().merge(optionIndex, 1, Integer::sum); + PollVote vote = new PollVote(); + vote.setPost(post); + vote.setUser(user); + vote.setOptionIndex(optionIndex); + pollVoteRepository.save(vote); + } PollPost saved = pollPostRepository.save(post); if (post.getAuthor() != null && !post.getAuthor().getId().equals(user.getId())) { notificationService.createNotification(post.getAuthor(), NotificationType.POLL_VOTE, post, null, null, user, null, null); diff --git a/backend/src/test/java/com/openisle/controller/PostControllerTest.java b/backend/src/test/java/com/openisle/controller/PostControllerTest.java index f55c2d497..5b667e926 100644 --- a/backend/src/test/java/com/openisle/controller/PostControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/PostControllerTest.java @@ -76,7 +76,7 @@ class PostControllerTest { post.setTags(Set.of(tag)); when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)), - isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post); + isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post); when(postService.viewPost(eq(1L), any())).thenReturn(post); when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of()); when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); @@ -187,7 +187,7 @@ class PostControllerTest { .andExpect(status().isBadRequest()); verify(postService, never()).createPost(any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any(), any()); + any(), any(), any(), any(), any(), any(), any(), any(), any()); } @Test diff --git a/backend/src/test/java/com/openisle/service/PostServiceTest.java b/backend/src/test/java/com/openisle/service/PostServiceTest.java index f21155f35..de3f58f43 100644 --- a/backend/src/test/java/com/openisle/service/PostServiceTest.java +++ b/backend/src/test/java/com/openisle/service/PostServiceTest.java @@ -146,7 +146,7 @@ class PostServiceTest { assertThrows(RateLimitException.class, () -> service.createPost("alice", 1L, "t", "c", List.of(1L), - null, null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null)); } @Test diff --git a/frontend_nuxt/components/PollForm.vue b/frontend_nuxt/components/PollForm.vue index fe8454505..15d0adf95 100644 --- a/frontend_nuxt/components/PollForm.vue +++ b/frontend_nuxt/components/PollForm.vue @@ -18,6 +18,12 @@ +
+ +
@@ -80,6 +86,13 @@ const removeOption = (idx) => { display: flex; flex-direction: column; } +.poll-multiple-row { + display: flex; + align-items: center; +} +.multiple-checkbox { + margin-right: 5px; +} .time-picker { max-width: 200px; height: 30px; diff --git a/frontend_nuxt/components/PostPoll.vue b/frontend_nuxt/components/PostPoll.vue index 2f021907e..d038957c4 100644 --- a/frontend_nuxt/components/PostPoll.vue +++ b/frontend_nuxt/components/PostPoll.vue @@ -29,23 +29,42 @@
-
- - {{ opt }} -
- -
-
- - 该投票为多选 + +
@@ -178,6 +197,40 @@ const voteOption = async (idx) => { toast.error(data.error || '操作失败') } } + +const selectedOptions = ref([]) +const toggleOption = (idx) => { + const i = selectedOptions.value.indexOf(idx) + if (i >= 0) { + selectedOptions.value.splice(i, 1) + } else { + selectedOptions.value.push(idx) + } +} +const submitMultiPoll = async () => { + const token = getToken() + if (!token) { + toast.error('请先登录') + return + } + if (!selectedOptions.value.length) { + toast.error('请选择至少一个选项') + return + } + const params = selectedOptions.value.map((o) => `option=${o}`).join('&') + const res = await fetch(`${API_BASE_URL}/api/posts/${props.postId}/poll/vote?${params}`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }) + const data = await res.json().catch(() => ({})) + if (res.ok) { + toast.success('投票成功') + emit('refresh') + showPollResult.value = true + } else { + toast.error(data.error || '操作失败') + } +}