mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-20 22:11:01 +08:00
Merge remote-tracking branch 'origin/main' into feat_category_proposal
This commit is contained in:
@@ -97,6 +97,8 @@ public class SecurityConfig {
|
||||
"http://localhost:8081",
|
||||
"http://localhost:8082",
|
||||
"http://localhost:3000",
|
||||
"http://frontend_dev:3000",
|
||||
"http://frontend_service:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost",
|
||||
"http://30.211.97.238:3000",
|
||||
@@ -177,6 +179,8 @@ public class SecurityConfig {
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/point-goods")
|
||||
.permitAll()
|
||||
.requestMatchers("/actuator/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/categories/**")
|
||||
.hasAuthority("ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/api/tags/**")
|
||||
@@ -230,6 +234,7 @@ public class SecurityConfig {
|
||||
uri.startsWith("/api/channels") ||
|
||||
uri.startsWith("/api/sitemap.xml") ||
|
||||
uri.startsWith("/api/medals") ||
|
||||
uri.startsWith("/actuator") ||
|
||||
uri.startsWith("/api/rss"));
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -131,6 +132,7 @@ public class CommentController {
|
||||
c.getId(),
|
||||
"comment",
|
||||
c.getCreatedAt(),
|
||||
c.getPinnedAt(),
|
||||
c // payload 是 CommentDto
|
||||
)
|
||||
)
|
||||
@@ -145,17 +147,39 @@ public class CommentController {
|
||||
l.getId(),
|
||||
"log",
|
||||
l.getTime(), // 注意字段名不一样
|
||||
null,
|
||||
l // payload 是 PostChangeLogDto
|
||||
)
|
||||
)
|
||||
.toList()
|
||||
);
|
||||
// 排序
|
||||
Comparator<TimelineItemDto<?>> comparator = Comparator.comparing(TimelineItemDto::getCreatedAt);
|
||||
Comparator<TimelineItemDto<?>> pinnedOrderComparator = (a, b) -> {
|
||||
LocalDateTime aPinned = a.getPinnedAt();
|
||||
LocalDateTime bPinned = b.getPinnedAt();
|
||||
if (aPinned == null && bPinned == null) {
|
||||
return 0;
|
||||
}
|
||||
if (aPinned == null) {
|
||||
return 1;
|
||||
}
|
||||
if (bPinned == null) {
|
||||
return -1;
|
||||
}
|
||||
return bPinned.compareTo(aPinned);
|
||||
};
|
||||
|
||||
Comparator<TimelineItemDto<?>> comparator = Comparator.<TimelineItemDto<?>, Boolean>comparing(
|
||||
item -> item.getPinnedAt() == null
|
||||
).thenComparing(pinnedOrderComparator);
|
||||
|
||||
Comparator<TimelineItemDto<?>> createdAtComparator = Comparator.comparing(
|
||||
TimelineItemDto::getCreatedAt
|
||||
);
|
||||
if (CommentSort.NEWEST.equals(sort)) {
|
||||
comparator = comparator.reversed();
|
||||
createdAtComparator = createdAtComparator.reversed();
|
||||
}
|
||||
itemDtoList.sort(comparator);
|
||||
itemDtoList.sort(comparator.thenComparing(createdAtComparator));
|
||||
log.debug("listComments returning {} comments", itemDtoList.size());
|
||||
return itemDtoList;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
16
backend/src/main/java/com/openisle/dto/DonationDto.java
Normal file
16
backend/src/main/java/com/openisle/dto/DonationDto.java
Normal file
@@ -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;
|
||||
}
|
||||
11
backend/src/main/java/com/openisle/dto/DonationRequest.java
Normal file
11
backend/src/main/java/com/openisle/dto/DonationRequest.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class DonationRequest {
|
||||
|
||||
private int amount;
|
||||
}
|
||||
15
backend/src/main/java/com/openisle/dto/DonationResponse.java
Normal file
15
backend/src/main/java/com/openisle/dto/DonationResponse.java
Normal file
@@ -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<DonationDto> donations = new ArrayList<>();
|
||||
private Integer balance;
|
||||
}
|
||||
@@ -29,4 +29,5 @@ public class PostChangeLogDto {
|
||||
private LocalDateTime newPinnedAt;
|
||||
private Boolean oldFeatured;
|
||||
private Boolean newFeatured;
|
||||
private Integer amount;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ public class TimelineItemDto<T> {
|
||||
private Long id;
|
||||
private String kind; // "comment" | "log"
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime pinnedAt;
|
||||
private T payload; // 泛型,具体类型由外部决定
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ public class PostChangeLogMapper {
|
||||
} else if (log instanceof PostFeaturedChangeLog f) {
|
||||
dto.setOldFeatured(f.isOldFeatured());
|
||||
dto.setNewFeatured(f.isNewFeatured());
|
||||
} else if (log instanceof PostDonateChangeLog d) {
|
||||
dto.setAmount(d.getAmount());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public enum PointHistoryType {
|
||||
REDEEM,
|
||||
LOTTERY_JOIN,
|
||||
LOTTERY_REWARD,
|
||||
DONATE_SENT,
|
||||
DONATE_RECEIVED,
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ public enum PostChangeType {
|
||||
FEATURED,
|
||||
VOTE_RESULT,
|
||||
LOTTERY_RESULT,
|
||||
DONATE,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table(name = "post_donate_change_logs")
|
||||
public class PostDonateChangeLog extends PostChangeLog {
|
||||
|
||||
@Column(nullable = false)
|
||||
private int amount;
|
||||
}
|
||||
@@ -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<PointHistory, Long> {
|
||||
List<PointHistory> findByUserOrderByIdDesc(User user);
|
||||
@@ -21,4 +24,11 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
|
||||
List<PointHistory> findByComment(Comment comment);
|
||||
|
||||
List<PointHistory> findByPost(Post post);
|
||||
|
||||
List<PointHistory> 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);
|
||||
}
|
||||
|
||||
@@ -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,8 @@ public class PointService {
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final PostChangeLogService postChangeLogService;
|
||||
|
||||
public int awardForPost(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
@@ -272,4 +278,95 @@ 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)
|
||||
);
|
||||
postChangeLogService.recordDonation(post, donor, 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<PointHistory> histories =
|
||||
pointHistoryRepository.findTop10ByPostAndTypeOrderByCreatedAtDesc(
|
||||
post,
|
||||
PointHistoryType.DONATE_RECEIVED
|
||||
);
|
||||
List<DonationDto> donations = histories
|
||||
.stream()
|
||||
.collect(Collectors.collectingAndThen(Collectors.toMap(
|
||||
history -> {
|
||||
User donor = history.getFromUser();
|
||||
if (donor != null && donor.getId() != null) {
|
||||
return "user:" + donor.getId();
|
||||
}
|
||||
return "history:" + history.getId();
|
||||
},
|
||||
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;
|
||||
},
|
||||
(left, right) -> {
|
||||
left.setAmount(left.getAmount() + right.getAmount());
|
||||
if (
|
||||
left.getCreatedAt() == null ||
|
||||
(right.getCreatedAt() != null && right.getCreatedAt().isAfter(left.getCreatedAt()))
|
||||
) {
|
||||
left.setCreatedAt(right.getCreatedAt());
|
||||
}
|
||||
return left;
|
||||
},
|
||||
java.util.LinkedHashMap::new
|
||||
), map -> new java.util.ArrayList<>(map.values())));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,15 @@ public class PostChangeLogService {
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void recordDonation(Post post, User donor, int amount) {
|
||||
PostDonateChangeLog log = new PostDonateChangeLog();
|
||||
log.setPost(post);
|
||||
log.setUser(donor);
|
||||
log.setType(PostChangeType.DONATE);
|
||||
log.setAmount(amount);
|
||||
logRepository.save(log);
|
||||
}
|
||||
|
||||
public void deleteLogsForPost(Post post) {
|
||||
logRepository.deleteByPost(post);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user