diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 3ea7334e2..aa1707ff1 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -41,7 +41,8 @@ public class PostController { Post post = postService.createPost(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds(), req.getType(), req.getPrizeDescription(), req.getPrizeIcon(), - req.getPrizeCount(), req.getStartTime(), req.getEndTime()); + req.getPrizeCount(), req.getPointCost(), + req.getStartTime(), req.getEndTime()); draftService.deleteDraft(auth.getName()); PostDetailDto dto = postMapper.toDetailDto(post, auth.getName()); dto.setReward(levelService.awardForPost(auth.getName())); diff --git a/backend/src/main/java/com/openisle/controller/UserController.java b/backend/src/main/java/com/openisle/controller/UserController.java index 01afbabb6..c3c737ce4 100644 --- a/backend/src/main/java/com/openisle/controller/UserController.java +++ b/backend/src/main/java/com/openisle/controller/UserController.java @@ -105,6 +105,17 @@ public class UserController { .collect(java.util.stream.Collectors.toList()); } + @GetMapping("/{identifier}/subscribed-posts") + public java.util.List subscribedPosts(@PathVariable("identifier") String identifier, + @RequestParam(value = "limit", required = false) Integer limit) { + int l = limit != null ? limit : defaultPostsLimit; + User user = userService.findByIdentifier(identifier).orElseThrow(); + return subscriptionService.getSubscribedPosts(user.getUsername()).stream() + .limit(l) + .map(userMapper::toMetaDto) + .collect(java.util.stream.Collectors.toList()); + } + @GetMapping("/{identifier}/replies") public java.util.List userReplies(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { diff --git a/backend/src/main/java/com/openisle/dto/LotteryDto.java b/backend/src/main/java/com/openisle/dto/LotteryDto.java index db6728993..73b5ca7d7 100644 --- a/backend/src/main/java/com/openisle/dto/LotteryDto.java +++ b/backend/src/main/java/com/openisle/dto/LotteryDto.java @@ -10,6 +10,7 @@ public class LotteryDto { private String prizeDescription; private String prizeIcon; private int prizeCount; + private int pointCost; private LocalDateTime startTime; private LocalDateTime endTime; private List participants; diff --git a/backend/src/main/java/com/openisle/dto/PostRequest.java b/backend/src/main/java/com/openisle/dto/PostRequest.java index 014d68bf7..1fe02669a 100644 --- a/backend/src/main/java/com/openisle/dto/PostRequest.java +++ b/backend/src/main/java/com/openisle/dto/PostRequest.java @@ -23,6 +23,7 @@ public class PostRequest { private String prizeDescription; private String prizeIcon; private Integer prizeCount; + private Integer pointCost; private LocalDateTime startTime; private LocalDateTime endTime; } diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index d36b25e32..ad1a826da 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -86,6 +86,7 @@ public class PostMapper { l.setPrizeDescription(lp.getPrizeDescription()); l.setPrizeIcon(lp.getPrizeIcon()); l.setPrizeCount(lp.getPrizeCount()); + l.setPointCost(lp.getPointCost()); l.setStartTime(lp.getStartTime()); l.setEndTime(lp.getEndTime()); l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); diff --git a/backend/src/main/java/com/openisle/model/LotteryPost.java b/backend/src/main/java/com/openisle/model/LotteryPost.java index 79639a1da..f7579714c 100644 --- a/backend/src/main/java/com/openisle/model/LotteryPost.java +++ b/backend/src/main/java/com/openisle/model/LotteryPost.java @@ -26,6 +26,9 @@ public class LotteryPost extends Post { @Column(nullable = false) private int prizeCount; + @Column(nullable = false) + private int pointCost; + @Column private LocalDateTime startTime; diff --git a/backend/src/main/java/com/openisle/model/PointHistoryType.java b/backend/src/main/java/com/openisle/model/PointHistoryType.java index af03d989c..5bff37197 100644 --- a/backend/src/main/java/com/openisle/model/PointHistoryType.java +++ b/backend/src/main/java/com/openisle/model/PointHistoryType.java @@ -8,5 +8,7 @@ public enum PointHistoryType { INVITE, FEATURE, SYSTEM_ONLINE, - REDEEM + REDEEM, + LOTTERY_JOIN, + LOTTERY_REWARD } diff --git a/backend/src/main/java/com/openisle/service/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index 2b5a53060..2e5fa48b7 100644 --- a/backend/src/main/java/com/openisle/service/PointService.java +++ b/backend/src/main/java/com/openisle/service/PointService.java @@ -2,6 +2,7 @@ package com.openisle.service; import com.openisle.model.*; import com.openisle.repository.*; +import com.openisle.exception.FieldException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -39,6 +40,17 @@ public class PointService { return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null); } + public void processLotteryJoin(User participant, LotteryPost post) { + int cost = post.getPointCost(); + if (cost > 0) { + if (participant.getPoint() < cost) { + throw new FieldException("point", "积分不足"); + } + addPoint(participant, -cost, PointHistoryType.LOTTERY_JOIN, post, null, post.getAuthor()); + addPoint(post.getAuthor(), cost, PointHistoryType.LOTTERY_REWARD, post, null, participant); + } + } + private PointLog getTodayLog(User user) { LocalDate today = LocalDate.now(); return pointLogRepository.findByUserAndLogDate(user, today) diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index a3038d478..993c19c20 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -164,6 +164,7 @@ public class PostService { String prizeDescription, String prizeIcon, Integer prizeCount, + Integer pointCost, LocalDateTime startTime, LocalDateTime endTime) { long recent = postRepository.countByAuthorAfter(username, @@ -188,10 +189,14 @@ public class PostService { PostType actualType = type != null ? type : PostType.NORMAL; Post post; if (actualType == PostType.LOTTERY) { + if (pointCost != null && (pointCost < 0 || pointCost > 100)) { + throw new IllegalArgumentException("pointCost must be between 0 and 100"); + } LotteryPost lp = new LotteryPost(); lp.setPrizeDescription(prizeDescription); lp.setPrizeIcon(prizeIcon); lp.setPrizeCount(prizeCount != null ? prizeCount : 0); + lp.setPointCost(pointCost != null ? pointCost : 0); lp.setStartTime(startTime); lp.setEndTime(endTime); post = lp; @@ -250,8 +255,10 @@ public class PostService { .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); - post.getParticipants().add(user); - lotteryPostRepository.save(post); + if (post.getParticipants().add(user)) { + pointService.processLotteryJoin(user, post); + lotteryPostRepository.save(post); + } } @Transactional diff --git a/backend/src/main/java/com/openisle/service/SubscriptionService.java b/backend/src/main/java/com/openisle/service/SubscriptionService.java index 841fad659..f6429ff31 100644 --- a/backend/src/main/java/com/openisle/service/SubscriptionService.java +++ b/backend/src/main/java/com/openisle/service/SubscriptionService.java @@ -107,6 +107,11 @@ public class SubscriptionService { return commentSubRepo.findByComment(c).stream().map(CommentSubscription::getUser).toList(); } + public List getSubscribedPosts(String username) { + User user = userRepo.findByUsername(username).orElseThrow(); + return postSubRepo.findByUser(user).stream().map(PostSubscription::getPost).toList(); + } + public long countSubscribers(String username) { User user = userRepo.findByUsername(username).orElseThrow(); diff --git a/backend/src/main/resources/db/migration/V3__add_point_cost_to_lottery_posts.sql b/backend/src/main/resources/db/migration/V3__add_point_cost_to_lottery_posts.sql new file mode 100644 index 000000000..115689a5c --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_point_cost_to_lottery_posts.sql @@ -0,0 +1 @@ +ALTER TABLE lottery_posts ADD COLUMN point_cost INT NOT NULL DEFAULT 0; diff --git a/backend/src/test/java/com/openisle/controller/PostControllerTest.java b/backend/src/test/java/com/openisle/controller/PostControllerTest.java index 7b4fc2265..90779b57e 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())).thenReturn(post); + 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()); } @Test diff --git a/backend/src/test/java/com/openisle/controller/UserControllerTest.java b/backend/src/test/java/com/openisle/controller/UserControllerTest.java index 4af99507c..22d48bd2c 100644 --- a/backend/src/test/java/com/openisle/controller/UserControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/UserControllerTest.java @@ -136,6 +136,30 @@ class UserControllerTest { .andExpect(jsonPath("$[0].title").value("hello")); } + @Test + void listSubscribedPosts() throws Exception { + User user = new User(); + user.setUsername("bob"); + com.openisle.model.Category cat = new com.openisle.model.Category(); + cat.setName("tech"); + com.openisle.model.Post post = new com.openisle.model.Post(); + post.setId(6L); + post.setTitle("fav"); + post.setCreatedAt(java.time.LocalDateTime.now()); + post.setCategory(cat); + post.setAuthor(user); + Mockito.when(userService.findByIdentifier("bob")).thenReturn(Optional.of(user)); + Mockito.when(subscriptionService.getSubscribedPosts("bob")).thenReturn(java.util.List.of(post)); + PostMetaDto meta = new PostMetaDto(); + meta.setId(6L); + meta.setTitle("fav"); + Mockito.when(userMapper.toMetaDto(post)).thenReturn(meta); + + mockMvc.perform(get("/api/users/bob/subscribed-posts")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].title").value("fav")); + } + @Test void listUserReplies() throws Exception { User user = new User(); diff --git a/backend/src/test/java/com/openisle/service/PostServiceTest.java b/backend/src/test/java/com/openisle/service/PostServiceTest.java index 6abe97238..a7bf71caa 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)); } @Test diff --git a/frontend_nuxt/components/MessageFloatWindow.vue b/frontend_nuxt/components/MessageFloatWindow.vue index 6b44ca6d4..fb2495ac7 100644 --- a/frontend_nuxt/components/MessageFloatWindow.vue +++ b/frontend_nuxt/components/MessageFloatWindow.vue @@ -9,14 +9,12 @@ title="收起至 100px" @click="collapseToMini" > - - @@ -61,7 +59,6 @@ function injectBaseTag() { } } -// 当浮窗重新出现时,恢复默认高度 watch( () => floatRoute.value, (v) => { @@ -76,7 +73,6 @@ watch( bottom: 0; right: 0; width: 400px; - /* 高度由内联样式绑定控制:60vh / 100px */ max-height: 90vh; background-color: var(--background-color); border: 1px solid var(--normal-border-color); diff --git a/frontend_nuxt/components/ReactionsGroup.vue b/frontend_nuxt/components/ReactionsGroup.vue index 697cc81e0..ee9774422 100644 --- a/frontend_nuxt/components/ReactionsGroup.vue +++ b/frontend_nuxt/components/ReactionsGroup.vue @@ -6,7 +6,7 @@ @mouseenter="cancelHide" @mouseleave="scheduleHide" > -