From 8643446d8b885f3e8be0796daddc623bdd53583b Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 17 Oct 2025 11:24:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=9E=E8=B5=8F=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PostDonationController.java | 43 +++++ .../java/com/openisle/dto/DonationDto.java | 16 ++ .../com/openisle/dto/DonationRequest.java | 11 ++ .../com/openisle/dto/DonationResponse.java | 15 ++ .../com/openisle/model/NotificationType.java | 2 + .../com/openisle/model/PointHistoryType.java | 2 + .../repository/PointHistoryRepository.java | 10 ++ .../com/openisle/service/PointService.java | 76 ++++++++ frontend_nuxt/components/DonateGroup.vue | 170 +++++++++++++----- frontend_nuxt/pages/message.vue | 21 +++ frontend_nuxt/pages/points.vue | 23 +++ frontend_nuxt/pages/posts/[id]/index.vue | 2 +- frontend_nuxt/utils/notification.js | 13 ++ 13 files changed, 359 insertions(+), 45 deletions(-) create mode 100644 backend/src/main/java/com/openisle/controller/PostDonationController.java create mode 100644 backend/src/main/java/com/openisle/dto/DonationDto.java create mode 100644 backend/src/main/java/com/openisle/dto/DonationRequest.java create mode 100644 backend/src/main/java/com/openisle/dto/DonationResponse.java diff --git a/backend/src/main/java/com/openisle/controller/PostDonationController.java b/backend/src/main/java/com/openisle/controller/PostDonationController.java new file mode 100644 index 000000000..b74239416 --- /dev/null +++ b/backend/src/main/java/com/openisle/controller/PostDonationController.java @@ -0,0 +1,43 @@ +package com.openisle.controller; + +import com.openisle.dto.DonationRequest; +import com.openisle.dto.DonationResponse; +import com.openisle.service.PointService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/posts/{postId}/donations") +@RequiredArgsConstructor +public class PostDonationController { + + private final PointService pointService; + + @GetMapping + @Operation(summary = "List donations", description = "Get recent donations for a post") + @ApiResponse(responseCode = "200", description = "Donation summary") + public DonationResponse list(@PathVariable Long postId) { + return pointService.getPostDonations(postId); + } + + @PostMapping + @SecurityRequirement(name = "JWT") + @Operation(summary = "Donate", description = "Donate points to the post author") + @ApiResponse(responseCode = "200", description = "Donation result") + public DonationResponse donate( + @PathVariable Long postId, + @RequestBody DonationRequest req, + Authentication auth + ) { + return pointService.donateToPost(auth.getName(), postId, req.getAmount()); + } +} diff --git a/backend/src/main/java/com/openisle/dto/DonationDto.java b/backend/src/main/java/com/openisle/dto/DonationDto.java new file mode 100644 index 000000000..460cec56b --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DonationDto.java @@ -0,0 +1,16 @@ +package com.openisle.dto; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DonationDto { + + private Long userId; + private String username; + private String avatar; + private int amount; + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/com/openisle/dto/DonationRequest.java b/backend/src/main/java/com/openisle/dto/DonationRequest.java new file mode 100644 index 000000000..14421e1e5 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DonationRequest.java @@ -0,0 +1,11 @@ +package com.openisle.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DonationRequest { + + private int amount; +} diff --git a/backend/src/main/java/com/openisle/dto/DonationResponse.java b/backend/src/main/java/com/openisle/dto/DonationResponse.java new file mode 100644 index 000000000..1a83be807 --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/DonationResponse.java @@ -0,0 +1,15 @@ +package com.openisle.dto; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DonationResponse { + + private int totalAmount; + private List donations = new ArrayList<>(); + private Integer balance; +} diff --git a/backend/src/main/java/com/openisle/model/NotificationType.java b/backend/src/main/java/com/openisle/model/NotificationType.java index 0e96cfc20..74262c9b2 100644 --- a/backend/src/main/java/com/openisle/model/NotificationType.java +++ b/backend/src/main/java/com/openisle/model/NotificationType.java @@ -48,6 +48,8 @@ public enum NotificationType { POLL_RESULT_PARTICIPANT, /** Your post was featured */ POST_FEATURED, + /** Someone donated to your post */ + DONATION, /** You were mentioned in a post or comment */ MENTION, } diff --git a/backend/src/main/java/com/openisle/model/PointHistoryType.java b/backend/src/main/java/com/openisle/model/PointHistoryType.java index 689f73c9c..776827f0a 100644 --- a/backend/src/main/java/com/openisle/model/PointHistoryType.java +++ b/backend/src/main/java/com/openisle/model/PointHistoryType.java @@ -13,4 +13,6 @@ public enum PointHistoryType { REDEEM, LOTTERY_JOIN, LOTTERY_REWARD, + DONATE_SENT, + DONATE_RECEIVED, } diff --git a/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java index 197ac1a45..3436466a3 100644 --- a/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java +++ b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java @@ -2,11 +2,14 @@ package com.openisle.repository; import com.openisle.model.Comment; import com.openisle.model.PointHistory; +import com.openisle.model.PointHistoryType; import com.openisle.model.Post; import com.openisle.model.User; import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PointHistoryRepository extends JpaRepository { List findByUserOrderByIdDesc(User user); @@ -21,4 +24,11 @@ public interface PointHistoryRepository extends JpaRepository findByComment(Comment comment); List findByPost(Post post); + + List findTop10ByPostAndTypeOrderByCreatedAtDesc(Post post, PointHistoryType type); + + @Query( + "SELECT COALESCE(SUM(ph.amount), 0) FROM PointHistory ph WHERE ph.post = :post AND ph.type = :type" + ) + Long sumAmountByPostAndType(@Param("post") Post post, @Param("type") PointHistoryType type); } diff --git a/backend/src/main/java/com/openisle/service/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index 0a8349a53..52a732512 100644 --- a/backend/src/main/java/com/openisle/service/PointService.java +++ b/backend/src/main/java/com/openisle/service/PointService.java @@ -1,5 +1,7 @@ package com.openisle.service; +import com.openisle.dto.DonationDto; +import com.openisle.dto.DonationResponse; import com.openisle.exception.FieldException; import com.openisle.model.*; import com.openisle.repository.*; @@ -8,8 +10,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -20,6 +24,7 @@ public class PointService { private final PostRepository postRepository; private final CommentRepository commentRepository; private final PointHistoryRepository pointHistoryRepository; + private final NotificationService notificationService; public int awardForPost(String userName, Long postId) { User user = userRepository.findByUsername(userName).orElseThrow(); @@ -272,4 +277,75 @@ public class PointService { User user = userRepository.findByUsername(userName).orElseThrow(); return recalculateUserPoints(user); } + + @Transactional + public DonationResponse donateToPost(String donorName, Long postId, int amount) { + if (amount <= 0) { + throw new FieldException("amount", "打赏积分必须大于0"); + } + User donor = userRepository.findByUsername(donorName).orElseThrow(); + Post post = postRepository.findById(postId).orElseThrow(); + User author = post.getAuthor(); + if (author.getId().equals(donor.getId())) { + throw new FieldException("post", "不能给自己打赏"); + } + if (donor.getPoint() < amount) { + throw new FieldException("point", "积分不足"); + } + addPoint(donor, -amount, PointHistoryType.DONATE_SENT, post, null, author); + addPoint(author, amount, PointHistoryType.DONATE_RECEIVED, post, null, donor); + notificationService.createNotification( + author, + NotificationType.DONATION, + post, + null, + null, + donor, + null, + String.valueOf(amount) + ); + DonationResponse response = buildDonationResponse(post); + response.setBalance(donor.getPoint()); + return response; + } + + public DonationResponse getPostDonations(Long postId) { + Post post = postRepository.findById(postId).orElseThrow(); + return buildDonationResponse(post); + } + + private DonationResponse buildDonationResponse(Post post) { + List histories = + pointHistoryRepository.findTop10ByPostAndTypeOrderByCreatedAtDesc( + post, + PointHistoryType.DONATE_RECEIVED + ); + List donations = histories + .stream() + .map(history -> { + DonationDto dto = new DonationDto(); + User donor = history.getFromUser(); + if (donor != null) { + dto.setUserId(donor.getId()); + dto.setUsername(donor.getUsername()); + dto.setAvatar(donor.getAvatar()); + } + dto.setAmount(history.getAmount()); + dto.setCreatedAt(history.getCreatedAt()); + return dto; + }) + .collect(Collectors.toList()); + Long total = pointHistoryRepository.sumAmountByPostAndType( + post, + PointHistoryType.DONATE_RECEIVED + ); + int safeTotal = 0; + if (total != null) { + safeTotal = total > Integer.MAX_VALUE ? Integer.MAX_VALUE : total.intValue(); + } + DonationResponse response = new DonationResponse(); + response.setDonations(donations); + response.setTotalAmount(safeTotal); + return response; + } } diff --git a/frontend_nuxt/components/DonateGroup.vue b/frontend_nuxt/components/DonateGroup.vue index 720236269..0a093774f 100644 --- a/frontend_nuxt/components/DonateGroup.vue +++ b/frontend_nuxt/components/DonateGroup.vue @@ -1,24 +1,29 @@