mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-27 16:40:50 +08:00
Compare commits
16 Commits
codex/add-
...
codex/shor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8544803e62 | ||
|
|
90eee03198 | ||
|
|
3f152906f2 | ||
|
|
ef71d0b3d4 | ||
|
|
6f80d139ba | ||
|
|
7454931fa5 | ||
|
|
0852664a82 | ||
|
|
5814fb673a | ||
|
|
4ee4266e3d | ||
|
|
6a27fbe1d7 | ||
|
|
38ff04c358 | ||
|
|
fc27200ac1 | ||
|
|
b1998be425 | ||
|
|
72adc5b232 | ||
|
|
d24e67de5d | ||
|
|
eefefac236 |
@@ -111,7 +111,7 @@ public class PostMapper {
|
|||||||
.collect(Collectors.groupingBy(PollVote::getOptionIndex,
|
.collect(Collectors.groupingBy(PollVote::getOptionIndex,
|
||||||
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
|
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
|
||||||
p.setOptionParticipants(optionParticipants);
|
p.setOptionParticipants(optionParticipants);
|
||||||
p.setMultiple(pp.isMultiple());
|
p.setMultiple(Boolean.TRUE.equals(pp.getMultiple()));
|
||||||
dto.setPoll(p);
|
dto.setPoll(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import java.time.LocalDateTime;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table(name = "comments")
|
@Table(name = "comments")
|
||||||
|
@SQLDelete(sql = "UPDATE comments SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||||
|
@Where(clause = "deleted_at IS NULL")
|
||||||
public class Comment {
|
public class Comment {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@@ -41,4 +45,7 @@ public class Comment {
|
|||||||
@Column
|
@Column
|
||||||
private LocalDateTime pinnedAt;
|
private LocalDateTime pinnedAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public class InviteToken {
|
|||||||
@Id
|
@Id
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short token used in invite links. Existing records may have this field null
|
||||||
|
* and fall back to {@link #token} for backward compatibility.
|
||||||
|
*/
|
||||||
|
@Column(unique = true)
|
||||||
|
private String shortToken;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
private User inviter;
|
private User inviter;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import jakarta.persistence.*;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import java.time.LocalDateTime;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table(name = "point_histories")
|
@Table(name = "point_histories")
|
||||||
|
@SQLDelete(sql = "UPDATE point_histories SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||||
|
@Where(clause = "deleted_at IS NULL")
|
||||||
public class PointHistory {
|
public class PointHistory {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@@ -46,4 +50,7 @@ public class PointHistory {
|
|||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class PollPost extends Post {
|
|||||||
private Set<User> participants = new HashSet<>();
|
private Set<User> participants = new HashSet<>();
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private boolean multiple = false;
|
private Boolean multiple = false;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
||||||
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
||||||
|
|
||||||
|
Optional<InviteToken> findByShortToken(String shortToken);
|
||||||
|
|
||||||
|
boolean existsByShortToken(String shortToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.openisle.repository;
|
|||||||
|
|
||||||
import com.openisle.model.PointHistory;
|
import com.openisle.model.PointHistory;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.model.Comment;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -12,4 +13,6 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
|
|||||||
long countByUser(User user);
|
long countByUser(User user);
|
||||||
|
|
||||||
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
||||||
|
|
||||||
|
List<PointHistory> findByComment(Comment comment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.openisle.repository.UserRepository;
|
|||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.CommentSubscriptionRepository;
|
import com.openisle.repository.CommentSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
@@ -37,6 +38,7 @@ public class CommentService {
|
|||||||
private final ReactionRepository reactionRepository;
|
private final ReactionRepository reactionRepository;
|
||||||
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
||||||
private final NotificationRepository notificationRepository;
|
private final NotificationRepository notificationRepository;
|
||||||
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -235,10 +237,14 @@ public class CommentService {
|
|||||||
for (Comment c : replies) {
|
for (Comment c : replies) {
|
||||||
deleteCommentCascade(c);
|
deleteCommentCascade(c);
|
||||||
}
|
}
|
||||||
|
// 逻辑删除相关的积分历史记录
|
||||||
|
pointHistoryRepository.findByComment(comment).forEach(pointHistoryRepository::delete);
|
||||||
|
// 删除其他相关数据
|
||||||
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
||||||
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
||||||
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
||||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||||
|
// 逻辑删除评论
|
||||||
commentRepository.delete(comment);
|
commentRepository.delete(comment);
|
||||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,33 +30,53 @@ public class InviteService {
|
|||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
return existing.get().getToken();
|
InviteToken inviteToken = existing.get();
|
||||||
|
return inviteToken.getShortToken() != null ? inviteToken.getShortToken() : inviteToken.getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = jwtService.generateInviteToken(username);
|
String token = jwtService.generateInviteToken(username);
|
||||||
|
String shortToken;
|
||||||
|
do {
|
||||||
|
shortToken = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||||
|
} while (inviteTokenRepository.existsByShortToken(shortToken));
|
||||||
|
|
||||||
InviteToken inviteToken = new InviteToken();
|
InviteToken inviteToken = new InviteToken();
|
||||||
inviteToken.setToken(token);
|
inviteToken.setToken(token);
|
||||||
|
inviteToken.setShortToken(shortToken);
|
||||||
inviteToken.setInviter(inviter);
|
inviteToken.setInviter(inviter);
|
||||||
inviteToken.setCreatedDate(today);
|
inviteToken.setCreatedDate(today);
|
||||||
inviteToken.setUsageCount(0);
|
inviteToken.setUsageCount(0);
|
||||||
inviteTokenRepository.save(inviteToken);
|
inviteTokenRepository.save(inviteToken);
|
||||||
return token;
|
return shortToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InviteValidateResult validate(String token) {
|
public InviteValidateResult validate(String token) {
|
||||||
if (token == null || token.isEmpty()) {
|
if (token == null || token.isEmpty()) {
|
||||||
return new InviteValidateResult(null, false);
|
return new InviteValidateResult(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
||||||
|
String realToken = token;
|
||||||
|
if (invite == null) {
|
||||||
|
invite = inviteTokenRepository.findByShortToken(token).orElse(null);
|
||||||
|
if (invite == null) {
|
||||||
|
return new InviteValidateResult(null, false);
|
||||||
|
}
|
||||||
|
realToken = invite.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jwtService.validateAndGetSubjectForInvite(token);
|
jwtService.validateAndGetSubjectForInvite(realToken);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return new InviteValidateResult(null, false);
|
return new InviteValidateResult(null, false);
|
||||||
}
|
}
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
|
||||||
return new InviteValidateResult(invite, invite != null && invite.getUsageCount() < 3);
|
return new InviteValidateResult(invite, invite.getUsageCount() < 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void consume(String token, String newUserName) {
|
public void consume(String token, String newUserName) {
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
InviteToken invite = inviteTokenRepository.findById(token)
|
||||||
|
.orElseGet(() -> inviteTokenRepository.findByShortToken(token).orElseThrow());
|
||||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||||
inviteTokenRepository.save(invite);
|
inviteTokenRepository.save(invite);
|
||||||
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add logical delete support for comments and point_histories tables
|
||||||
|
|
||||||
|
-- Add deleted_at column to comments table
|
||||||
|
ALTER TABLE comments ADD COLUMN deleted_at DATETIME(6) NULL;
|
||||||
|
|
||||||
|
-- Add deleted_at column to point_histories table
|
||||||
|
ALTER TABLE point_histories ADD COLUMN deleted_at DATETIME(6) NULL;
|
||||||
|
|
||||||
|
-- Add index for better performance on logical delete queries
|
||||||
|
CREATE INDEX idx_comments_deleted_at ON comments(deleted_at);
|
||||||
|
CREATE INDEX idx_point_histories_deleted_at ON point_histories(deleted_at);
|
||||||
@@ -6,6 +6,7 @@ import com.openisle.repository.UserRepository;
|
|||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.CommentSubscriptionRepository;
|
import com.openisle.repository.CommentSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -24,10 +25,11 @@ class CommentServiceTest {
|
|||||||
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
||||||
CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class);
|
CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class);
|
||||||
NotificationRepository nRepo = mock(NotificationRepository.class);
|
NotificationRepository nRepo = mock(NotificationRepository.class);
|
||||||
|
PointHistoryRepository pointHistoryRepo = mock(PointHistoryRepository.class);
|
||||||
ImageUploader imageUploader = mock(ImageUploader.class);
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
|
||||||
CommentService service = new CommentService(commentRepo, postRepo, userRepo,
|
CommentService service = new CommentService(commentRepo, postRepo, userRepo,
|
||||||
notifService, subService, reactionRepo, subRepo, nRepo, imageUploader);
|
notifService, subService, reactionRepo, subRepo, nRepo, pointHistoryRepo, imageUploader);
|
||||||
|
|
||||||
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -37,7 +39,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -69,6 +71,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -84,7 +88,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -122,6 +126,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -137,7 +143,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -156,6 +162,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -171,7 +179,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|||||||
@@ -19,10 +19,8 @@
|
|||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-multiple-row">
|
<div class="poll-multiple-row">
|
||||||
<label class="poll-row-title">
|
<span class="poll-row-title">多选</span>
|
||||||
<input type="checkbox" v-model="data.multiple" class="multiple-checkbox" />
|
<BaseSwitch v-model="data.multiple" />
|
||||||
多选
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,6 +29,7 @@
|
|||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
import FlatPickr from 'vue-flatpickr-component'
|
import FlatPickr from 'vue-flatpickr-component'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
|
import BaseSwitch from '~/components/BaseSwitch.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -89,9 +88,7 @@ const removeOption = (idx) => {
|
|||||||
.poll-multiple-row {
|
.poll-multiple-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
gap: 10px;
|
||||||
.multiple-checkbox {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
}
|
||||||
.time-picker {
|
.time-picker {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
|||||||
@@ -29,6 +29,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
<div class="poll-title-section">
|
||||||
|
<div class="poll-option-title" v-if="poll.multiple">多选</div>
|
||||||
|
<div class="poll-option-title" v-else>单选</div>
|
||||||
|
|
||||||
|
<div class="poll-left-time">
|
||||||
|
<div class="poll-left-time-title">离结束还有</div>
|
||||||
|
<div class="poll-left-time-value">{{ countdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template v-if="poll.multiple">
|
<template v-if="poll.multiple">
|
||||||
<div
|
<div
|
||||||
v-for="(opt, idx) in poll.options"
|
v-for="(opt, idx) in poll.options"
|
||||||
@@ -45,12 +54,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="multi-selection-container">
|
<div class="multi-selection-container">
|
||||||
<div class="multi-selection-title">
|
|
||||||
<i class="fas fa-info-circle info-icon"></i>
|
|
||||||
该投票为多选
|
|
||||||
</div>
|
|
||||||
<div class="join-poll-button" @click="submitMultiPoll">
|
<div class="join-poll-button" @click="submitMultiPoll">
|
||||||
<i class="fas fa-plus"></i> 加入投票
|
<i class="fas fa-check"></i> 确认投票
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,11 +64,22 @@
|
|||||||
v-for="(opt, idx) in poll.options"
|
v-for="(opt, idx) in poll.options"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="poll-option"
|
class="poll-option"
|
||||||
@click="voteOption(idx)"
|
@click="selectOption(idx)"
|
||||||
>
|
>
|
||||||
<input type="radio" :checked="false" name="poll-option" class="poll-option-input" />
|
<input
|
||||||
|
type="radio"
|
||||||
|
:checked="selectedOption === idx"
|
||||||
|
name="poll-option"
|
||||||
|
class="poll-option-input"
|
||||||
|
/>
|
||||||
<span class="poll-option-text">{{ opt }}</span>
|
<span class="poll-option-text">{{ opt }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="single-selection-container">
|
||||||
|
<div class="join-poll-button" @click="submitSinglePoll">
|
||||||
|
<i class="fas fa-check"></i> 确认投票
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,10 +103,11 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-chart-bar"></i> 结果
|
<i class="fas fa-chart-bar"></i> 结果
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="pollEnded" class="poll-option-hint">
|
||||||
<div class="poll-left-time">
|
<i class="fas fa-stopwatch"></i> 投票已结束
|
||||||
<div class="poll-left-time-title">离结束还有</div>
|
</div>
|
||||||
<div class="poll-left-time-value">{{ countdown }}</div>
|
<div v-else class="poll-option-hint">
|
||||||
|
<i class="fas fa-stopwatch"></i> 您已投票,等待结束查看结果
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,6 +215,18 @@ const voteOption = async (idx) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedOption = ref(null)
|
||||||
|
const selectOption = (idx) => {
|
||||||
|
selectedOption.value = idx
|
||||||
|
}
|
||||||
|
const submitSinglePoll = async () => {
|
||||||
|
if (selectedOption.value === null) {
|
||||||
|
toast.error('请选择一个选项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await voteOption(selectedOption.value)
|
||||||
|
}
|
||||||
|
|
||||||
const selectedOptions = ref([])
|
const selectedOptions = ref([])
|
||||||
const toggleOption = (idx) => {
|
const toggleOption = (idx) => {
|
||||||
const i = selectedOptions.value.indexOf(idx)
|
const i = selectedOptions.value.indexOf(idx)
|
||||||
@@ -368,18 +397,37 @@ const submitMultiPoll = async () => {
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-selection-container {
|
.multi-selection-container,
|
||||||
padding: 20px 15px 20px 5px;
|
.single-selection-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-selection-title {
|
.multi-selection-title,
|
||||||
|
.single-selection-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-title-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-left-time {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user