fix: 修复代码合并问题

This commit is contained in:
sivdead
2025-09-25 18:17:26 +08:00
parent 76962d6d1c
commit 3da5d24488
3 changed files with 139 additions and 119 deletions

View File

@@ -1,10 +1,17 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig; import com.openisle.config.CachingConfig;
import com.openisle.exception.NotFoundException;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import com.openisle.model.*; import com.openisle.model.*;
import com.openisle.repository.*; import com.openisle.repository.*;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -21,36 +28,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import com.openisle.exception.RateLimitException;
import com.openisle.mapper.PostMapper;
import com.openisle.model.*;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.CommentRepository;
import com.openisle.repository.LotteryPostRepository;
import com.openisle.repository.NotificationRepository;
import com.openisle.repository.PointHistoryRepository;
import com.openisle.repository.PollPostRepository;
import com.openisle.repository.PollVoteRepository;
import com.openisle.repository.PostRepository;
import com.openisle.repository.PostSubscriptionRepository;
import com.openisle.repository.ReactionRepository;
import com.openisle.repository.TagRepository;
import com.openisle.repository.UserRepository;
import com.openisle.service.EmailSender;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
public class PostService { public class PostService {
@@ -121,6 +98,7 @@ public class PostService {
this.tagRepository = tagRepository; this.tagRepository = tagRepository;
this.lotteryPostRepository = lotteryPostRepository; this.lotteryPostRepository = lotteryPostRepository;
this.pollPostRepository = pollPostRepository; this.pollPostRepository = pollPostRepository;
this.categoryProposalPostRepository = categoryProposalPostRepository;
this.pollVoteRepository = pollVoteRepository; this.pollVoteRepository = pollVoteRepository;
this.notificationService = notificationService; this.notificationService = notificationService;
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
@@ -165,18 +143,23 @@ public class PostService {
for (PollPost pp : pollPostRepository.findByEndTimeBeforeAndResultAnnouncedFalse(now)) { for (PollPost pp : pollPostRepository.findByEndTimeBeforeAndResultAnnouncedFalse(now)) {
applicationContext.getBean(PostService.class).finalizePoll(pp.getId()); applicationContext.getBean(PostService.class).finalizePoll(pp.getId());
} }
for (CategoryProposalPost cp : categoryProposalPostRepository for (CategoryProposalPost cp : categoryProposalPostRepository.findByEndTimeAfterAndProposalStatus(
.findByEndTimeAfterAndProposalStatus(now, CategoryProposalStatus.PENDING)) { now,
if (cp.getEndTime() != null) { CategoryProposalStatus.PENDING
ScheduledFuture<?> future = taskScheduler.schedule( )) {
() -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()), if (cp.getEndTime() != null) {
java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant())); ScheduledFuture<?> future = taskScheduler.schedule(
scheduledFinalizations.put(cp.getId(), future); () -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()),
} java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant())
);
scheduledFinalizations.put(cp.getId(), future);
}
} }
for (CategoryProposalPost cp : categoryProposalPostRepository for (CategoryProposalPost cp : categoryProposalPostRepository.findByEndTimeBeforeAndProposalStatus(
.findByEndTimeBeforeAndProposalStatus(now, CategoryProposalStatus.PENDING)) { now,
applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()); CategoryProposalStatus.PENDING
)) {
applicationContext.getBean(PostService.class).finalizeProposal(cp.getId());
} }
} }
@@ -302,41 +285,41 @@ public class PostService {
pp.setMultiple(multiple != null && multiple); pp.setMultiple(multiple != null && multiple);
post = pp; post = pp;
} else if (actualType == PostType.PROPOSAL) { } else if (actualType == PostType.PROPOSAL) {
CategoryProposalPost cp = new CategoryProposalPost(); CategoryProposalPost cp = new CategoryProposalPost();
if (proposedName == null || proposedName.isBlank()) { if (proposedName == null || proposedName.isBlank()) {
throw new IllegalArgumentException("Proposed name required"); throw new IllegalArgumentException("Proposed name required");
}
if (proposedSlug == null || proposedSlug.isBlank()) {
throw new IllegalArgumentException("Proposed slug required");
}
if (categoryProposalPostRepository.existsByProposedSlug(proposedSlug)) {
throw new IllegalArgumentException("Proposed slug already exists: " + proposedSlug);
}
cp.setProposedName(proposedName);
cp.setProposedSlug(proposedSlug);
cp.setDescription(proposalDescription);
if (approveThreshold != null) {
if (approveThreshold < 0 || approveThreshold > 100) {
throw new IllegalArgumentException("approveThreshold must be between 0 and 100");
} }
if (proposedSlug == null || proposedSlug.isBlank()) { cp.setApproveThreshold(approveThreshold);
throw new IllegalArgumentException("Proposed slug required"); }
if (quorum != null) {
if (quorum < 0) {
throw new IllegalArgumentException("quorum must be >= 0");
} }
if (categoryProposalPostRepository.existsByProposedSlug(proposedSlug)) { cp.setQuorum(quorum);
throw new IllegalArgumentException("Proposed slug already exists: " + proposedSlug); }
} cp.setStartAt(startTime);
cp.setProposedName(proposedName); cp.setEndTime(endTime);
cp.setProposedSlug(proposedSlug); // default yes/no options if not provided
cp.setDescription(proposalDescription); if (options == null || options.size() < 2) {
if (approveThreshold != null) { cp.setOptions(List.of("同意", "反对"));
if (approveThreshold < 0 || approveThreshold > 100) { } else {
throw new IllegalArgumentException("approveThreshold must be between 0 and 100"); cp.setOptions(options);
} }
cp.setApproveThreshold(approveThreshold); cp.setMultiple(false);
} post = cp;
if (quorum != null) {
if (quorum < 0) {
throw new IllegalArgumentException("quorum must be >= 0");
}
cp.setQuorum(quorum);
}
cp.setStartAt(startTime);
cp.setEndTime(endTime);
// default yes/no options if not provided
if (options == null || options.size() < 2) {
cp.setOptions(List.of("同意", "反对"));
} else {
cp.setOptions(options);
}
cp.setMultiple(false);
post = cp;
} else { } else {
post = new Post(); post = new Post();
} }
@@ -349,8 +332,8 @@ public class PostService {
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED); post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
if (post instanceof LotteryPost) { if (post instanceof LotteryPost) {
post = lotteryPostRepository.save((LotteryPost) post); post = lotteryPostRepository.save((LotteryPost) post);
}else if (post instanceof CategoryProposalPost categoryProposalPost) { } else if (post instanceof CategoryProposalPost categoryProposalPost) {
post = categoryProposalPostRepository.save(categoryProposalPost); post = categoryProposalPostRepository.save(categoryProposalPost);
} else if (post instanceof PollPost) { } else if (post instanceof PollPost) {
post = pollPostRepository.save((PollPost) post); post = pollPostRepository.save((PollPost) post);
} else { } else {
@@ -406,10 +389,11 @@ public class PostService {
); );
scheduledFinalizations.put(lp.getId(), future); scheduledFinalizations.put(lp.getId(), future);
} else if (post instanceof CategoryProposalPost cp && cp.getEndTime() != null) { } else if (post instanceof CategoryProposalPost cp && cp.getEndTime() != null) {
ScheduledFuture<?> future = taskScheduler.schedule( ScheduledFuture<?> future = taskScheduler.schedule(
() -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()), () -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()),
java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant())); java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant())
scheduledFinalizations.put(cp.getId(), future); );
scheduledFinalizations.put(cp.getId(), future);
} else if (post instanceof PollPost pp && pp.getEndTime() != null) { } else if (post instanceof PollPost pp && pp.getEndTime() != null) {
ScheduledFuture<?> future = taskScheduler.schedule( ScheduledFuture<?> future = taskScheduler.schedule(
() -> applicationContext.getBean(PostService.class).finalizePoll(pp.getId()), () -> applicationContext.getBean(PostService.class).finalizePoll(pp.getId()),
@@ -420,47 +404,72 @@ public class PostService {
return post; return post;
} }
@CacheEvict( @CacheEvict(value = CachingConfig.POST_CACHE_NAME, allEntries = true)
value = CachingConfig.POST_CACHE_NAME, allEntries = true
)
@Transactional @Transactional
public void finalizeProposal(Long postId) { public void finalizeProposal(Long postId) {
scheduledFinalizations.remove(postId); scheduledFinalizations.remove(postId);
categoryProposalPostRepository.findById(postId).ifPresent(cp -> { categoryProposalPostRepository
if (cp.getProposalStatus() != CategoryProposalStatus.PENDING) { .findById(postId)
return; .ifPresent(cp -> {
} if (cp.getProposalStatus() != CategoryProposalStatus.PENDING) {
int totalParticipants = cp.getParticipants() != null ? cp.getParticipants().size() : 0; return;
int approveVotes = 0; }
if (cp.getVotes() != null) { int totalParticipants = cp.getParticipants() != null ? cp.getParticipants().size() : 0;
approveVotes = cp.getVotes().getOrDefault(0, 0); int approveVotes = 0;
} if (cp.getVotes() != null) {
boolean quorumMet = totalParticipants >= cp.getQuorum(); approveVotes = cp.getVotes().getOrDefault(0, 0);
int approvePercent = totalParticipants > 0 ? (approveVotes * 100) / totalParticipants : 0; }
boolean thresholdMet = approvePercent >= cp.getApproveThreshold(); boolean quorumMet = totalParticipants >= cp.getQuorum();
if (quorumMet && thresholdMet) { int approvePercent = totalParticipants > 0 ? (approveVotes * 100) / totalParticipants : 0;
cp.setProposalStatus(CategoryProposalStatus.APPROVED); boolean thresholdMet = approvePercent >= cp.getApproveThreshold();
} else { if (quorumMet && thresholdMet) {
cp.setProposalStatus(CategoryProposalStatus.REJECTED); cp.setProposalStatus(CategoryProposalStatus.APPROVED);
String reason; } else {
if (!quorumMet && !thresholdMet) { cp.setProposalStatus(CategoryProposalStatus.REJECTED);
reason = "未达到法定人数且赞成率不足"; String reason;
} else if (!quorumMet) { if (!quorumMet && !thresholdMet) {
reason = "未达到法定人数"; reason = "未达到法定人数且赞成率不足";
} else { } else if (!quorumMet) {
reason = "赞成率不足"; reason = "未达到法定人数";
} } else {
cp.setRejectReason(reason); reason = "赞成率不足";
} }
cp.setResultSnapshot("approveVotes=" + approveVotes + ", totalParticipants=" + totalParticipants + ", approvePercent=" + approvePercent); cp.setRejectReason(reason);
categoryProposalPostRepository.save(cp); }
if (cp.getAuthor() != null) { cp.setResultSnapshot(
notificationService.createNotification(cp.getAuthor(), NotificationType.POLL_RESULT_OWNER, cp, null, null, null, null, null); "approveVotes=" +
} approveVotes +
for (User participant : cp.getParticipants()) { ", totalParticipants=" +
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, cp, null, null, null, null, null); totalParticipants +
} ", approvePercent=" +
postChangeLogService.recordVoteResult(cp); approvePercent
);
categoryProposalPostRepository.save(cp);
if (cp.getAuthor() != null) {
notificationService.createNotification(
cp.getAuthor(),
NotificationType.POLL_RESULT_OWNER,
cp,
null,
null,
null,
null,
null
);
}
for (User participant : cp.getParticipants()) {
notificationService.createNotification(
participant,
NotificationType.POLL_RESULT_PARTICIPANT,
cp,
null,
null,
null,
null,
null
);
}
postChangeLogService.recordVoteResult(cp);
}); });
} }

View File

@@ -76,6 +76,15 @@ class PostControllerTest {
@MockBean @MockBean
private MedalService medalService; private MedalService medalService;
@MockBean
private CategoryService categoryService;
@MockBean
private TagService tagService;
@MockBean
private PointService pointService;
@MockBean @MockBean
private com.openisle.repository.PollVoteRepository pollVoteRepository; private com.openisle.repository.PollVoteRepository pollVoteRepository;
@@ -275,6 +284,7 @@ class PostControllerTest {
any(), any(),
any(), any(),
any(), any(),
any(),
any() any()
); );
} }

View File

@@ -46,3 +46,4 @@ app.avatar.base-url=${AVATAR_BASE_URL:https://api.dicebear.com/6.x}
# Web push configuration # Web push configuration
app.webpush.public-key=${WEBPUSH_PUBLIC_KEY:} app.webpush.public-key=${WEBPUSH_PUBLIC_KEY:}
app.webpush.private-key=${WEBPUSH_PRIVATE_KEY:} app.webpush.private-key=${WEBPUSH_PRIVATE_KEY:}
app.snippet-length=${SNIPPET_LENGTH:200}