feat: implement lottery post type

This commit is contained in:
Tim
2025-08-11 01:17:55 +08:00
parent 4441c697b3
commit eb32e4bad7
11 changed files with 221 additions and 7 deletions

View File

@@ -0,0 +1,20 @@
package com.openisle.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.TaskScheduler;
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(2);
scheduler.setThreadNamePrefix("lottery-");
scheduler.initialize();
return scheduler;
}
}

View File

@@ -39,7 +39,9 @@ public class PostController {
return ResponseEntity.badRequest().build();
}
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
req.getTitle(), req.getContent(), req.getTagIds());
req.getTitle(), req.getContent(), req.getTagIds(),
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
req.getPrizeCount(), req.getStartTime(), req.getEndTime());
draftService.deleteDraft(auth.getName());
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
dto.setReward(levelService.awardForPost(auth.getName()));
@@ -67,6 +69,12 @@ public class PostController {
return ResponseEntity.ok(postMapper.toDetailDto(post, viewer));
}
@PostMapping("/{id}/lottery/join")
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
postService.joinLottery(id, auth.getName());
return ResponseEntity.ok().build();
}
@GetMapping
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,

View File

@@ -0,0 +1,17 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/** Metadata for lottery posts. */
@Data
public class LotteryDto {
private String prizeDescription;
private String prizeIcon;
private int prizeCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
private List<AuthorDto> participants;
private List<AuthorDto> winners;
}

View File

@@ -2,8 +2,11 @@ package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import com.openisle.model.PostType;
/**
* Request body for creating or updating a post.
*/
@@ -14,5 +17,13 @@ public class PostRequest {
private String content;
private List<Long> tagIds;
private String captcha;
// optional for lottery posts
private PostType type;
private String prizeDescription;
private String prizeIcon;
private Integer prizeCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
}

View File

@@ -1,6 +1,7 @@
package com.openisle.dto;
import com.openisle.model.PostStatus;
import com.openisle.model.PostType;
import lombok.Data;
import java.time.LocalDateTime;
@@ -28,5 +29,7 @@ public class PostSummaryDto {
private boolean subscribed;
private int reward;
private int pointReward;
private PostType type;
private LotteryDto lottery;
}

View File

@@ -4,8 +4,10 @@ import com.openisle.dto.CommentDto;
import com.openisle.dto.PostDetailDto;
import com.openisle.dto.PostSummaryDto;
import com.openisle.dto.ReactionDto;
import com.openisle.dto.LotteryDto;
import com.openisle.model.CommentSort;
import com.openisle.model.Post;
import com.openisle.model.LotteryPost;
import com.openisle.model.User;
import com.openisle.service.CommentService;
import com.openisle.service.ReactionService;
@@ -75,5 +77,18 @@ public class PostMapper {
dto.setLastReplyAt(last != null ? last : post.getCreatedAt());
dto.setReward(0);
dto.setSubscribed(false);
dto.setType(post.getType());
if (post instanceof LotteryPost lp) {
LotteryDto l = new LotteryDto();
l.setPrizeDescription(lp.getPrizeDescription());
l.setPrizeIcon(lp.getPrizeIcon());
l.setPrizeCount(lp.getPrizeCount());
l.setStartTime(lp.getStartTime());
l.setEndTime(lp.getEndTime());
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
l.setWinners(lp.getWinners().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
dto.setLottery(l);
}
}
}

View File

@@ -0,0 +1,46 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "lottery_posts")
@Getter
@Setter
@NoArgsConstructor
@PrimaryKeyJoinColumn(name = "post_id")
public class LotteryPost extends Post {
@Column
private String prizeDescription;
@Column
private String prizeIcon;
@Column(nullable = false)
private int prizeCount;
@Column
private LocalDateTime startTime;
@Column
private LocalDateTime endTime;
@ManyToMany
@JoinTable(name = "lottery_participants",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> participants = new HashSet<>();
@ManyToMany
@JoinTable(name = "lottery_winners",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> winners = new HashSet<>();
}

View File

@@ -9,11 +9,11 @@ import org.hibernate.annotations.CreationTimestamp;
import java.util.HashSet;
import java.util.Set;
import java.time.LocalDateTime;
import com.openisle.model.Tag;
import java.time.LocalDateTime;
/**
* Post entity representing an article posted by a user.
*/
@@ -22,6 +22,7 @@ import java.time.LocalDateTime;
@Setter
@NoArgsConstructor
@Table(name = "posts")
@Inheritance(strategy = InheritanceType.JOINED)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -59,6 +60,10 @@ public class Post {
@Column(nullable = false)
private PostStatus status = PostStatus.PUBLISHED;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostType type = PostType.NORMAL;
@Column
private LocalDateTime pinnedAt;

View File

@@ -0,0 +1,6 @@
package com.openisle.model;
public enum PostType {
NORMAL,
LOTTERY
}

View File

@@ -0,0 +1,7 @@
package com.openisle.repository;
import com.openisle.model.LotteryPost;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LotteryPostRepository extends JpaRepository<LotteryPost, Long> {
}

View File

@@ -2,12 +2,15 @@ package com.openisle.service;
import com.openisle.model.Post;
import com.openisle.model.PostStatus;
import com.openisle.model.PostType;
import com.openisle.model.PublishMode;
import com.openisle.model.User;
import com.openisle.model.Category;
import com.openisle.model.Comment;
import com.openisle.model.NotificationType;
import com.openisle.model.LotteryPost;
import com.openisle.repository.PostRepository;
import com.openisle.repository.LotteryPostRepository;
import com.openisle.repository.UserRepository;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
@@ -21,6 +24,8 @@ import com.openisle.model.Role;
import com.openisle.exception.RateLimitException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.scheduling.TaskScheduler;
import com.openisle.service.EmailSender;
import java.util.List;
import org.springframework.data.domain.PageRequest;
@@ -28,12 +33,19 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import java.time.ZoneId;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@Service
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final CategoryRepository categoryRepository;
private final TagRepository tagRepository;
private final LotteryPostRepository lotteryPostRepository;
private PublishMode publishMode;
private final NotificationService notificationService;
private final SubscriptionService subscriptionService;
@@ -44,12 +56,15 @@ public class PostService {
private final NotificationRepository notificationRepository;
private final PostReadService postReadService;
private final ImageUploader imageUploader;
private final TaskScheduler taskScheduler;
private final EmailSender emailSender;
@org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository,
UserRepository userRepository,
CategoryRepository categoryRepository,
TagRepository tagRepository,
LotteryPostRepository lotteryPostRepository,
NotificationService notificationService,
SubscriptionService subscriptionService,
CommentService commentService,
@@ -59,11 +74,14 @@ public class PostService {
NotificationRepository notificationRepository,
PostReadService postReadService,
ImageUploader imageUploader,
TaskScheduler taskScheduler,
EmailSender emailSender,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
this.postRepository = postRepository;
this.userRepository = userRepository;
this.categoryRepository = categoryRepository;
this.tagRepository = tagRepository;
this.lotteryPostRepository = lotteryPostRepository;
this.notificationService = notificationService;
this.subscriptionService = subscriptionService;
this.commentService = commentService;
@@ -73,6 +91,8 @@ public class PostService {
this.notificationRepository = notificationRepository;
this.postReadService = postReadService;
this.imageUploader = imageUploader;
this.taskScheduler = taskScheduler;
this.emailSender = emailSender;
this.publishMode = publishMode;
}
@@ -88,7 +108,13 @@ public class PostService {
Long categoryId,
String title,
String content,
java.util.List<Long> tagIds) {
java.util.List<Long> tagIds,
PostType type,
String prizeDescription,
String prizeIcon,
Integer prizeCount,
LocalDateTime startTime,
LocalDateTime endTime) {
long recent = postRepository.countByAuthorAfter(username,
java.time.LocalDateTime.now().minusMinutes(5));
if (recent >= 1) {
@@ -108,14 +134,31 @@ public class PostService {
if (tags.isEmpty()) {
throw new IllegalArgumentException("Tag not found");
}
Post post = new Post();
PostType actualType = type != null ? type : PostType.NORMAL;
Post post;
if (actualType == PostType.LOTTERY) {
LotteryPost lp = new LotteryPost();
lp.setPrizeDescription(prizeDescription);
lp.setPrizeIcon(prizeIcon);
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
lp.setStartTime(startTime);
lp.setEndTime(endTime);
post = lp;
} else {
post = new Post();
}
post.setType(actualType);
post.setTitle(title);
post.setContent(content);
post.setAuthor(author);
post.setCategory(category);
post.setTags(new java.util.HashSet<>(tags));
post.setTags(new HashSet<>(tags));
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
post = postRepository.save(post);
if (post instanceof LotteryPost) {
post = lotteryPostRepository.save((LotteryPost) post);
} else {
post = postRepository.save(post);
}
imageUploader.addReferences(imageUploader.extractUrls(content));
if (post.getStatus() == PostStatus.PENDING) {
java.util.List<User> admins = userRepository.findByRole(com.openisle.model.Role.ADMIN);
@@ -141,9 +184,42 @@ public class PostService {
}
}
notificationService.notifyMentions(content, author, post, null);
if (post instanceof LotteryPost lp && lp.getEndTime() != null) {
taskScheduler.schedule(() -> finalizeLottery(lp.getId()),
java.util.Date.from(lp.getEndTime().atZone(ZoneId.systemDefault()).toInstant()));
}
return post;
}
public void joinLottery(Long postId, String username) {
LotteryPost post = lotteryPostRepository.findById(postId)
.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);
}
private void finalizeLottery(Long postId) {
lotteryPostRepository.findById(postId).ifPresent(lp -> {
List<User> participants = new ArrayList<>(lp.getParticipants());
if (participants.isEmpty()) {
return;
}
Collections.shuffle(participants);
int winnersCount = Math.min(lp.getPrizeCount(), participants.size());
java.util.Set<User> winners = new java.util.HashSet<>(participants.subList(0, winnersCount));
lp.setWinners(winners);
lotteryPostRepository.save(lp);
for (User w : winners) {
if (w.getEmail() != null) {
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
}
}
});
}
@Transactional
public Post viewPost(Long id, String viewer) {
Post post = postRepository.findById(id)