fix: 完善提案通知流程,防止重复通知,提案成功自动插入分类

This commit is contained in:
Tim
2025-10-23 15:29:55 +08:00
parent 2271bbbd1d
commit 9c5a49a47f
5 changed files with 88 additions and 20 deletions

View File

@@ -46,6 +46,10 @@ public enum NotificationType {
POLL_RESULT_OWNER, POLL_RESULT_OWNER,
/** A poll you participated in has concluded */ /** A poll you participated in has concluded */
POLL_RESULT_PARTICIPANT, POLL_RESULT_PARTICIPANT,
/** Your category proposal has concluded */
CATEGORY_PROPOSAL_RESULT_OWNER,
/** A category proposal you participated in has concluded */
CATEGORY_PROPOSAL_RESULT_PARTICIPANT,
/** Your post was featured */ /** Your post was featured */
POST_FEATURED, POST_FEATURED,
/** Someone donated to your post */ /** Someone donated to your post */

View File

@@ -70,6 +70,7 @@ public class PostService {
private final PointService pointService; private final PointService pointService;
private final PostChangeLogService postChangeLogService; private final PostChangeLogService postChangeLogService;
private final PointHistoryRepository pointHistoryRepository; private final PointHistoryRepository pointHistoryRepository;
private final CategoryService categoryService;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
@@ -112,7 +113,8 @@ public class PostService {
PointHistoryRepository pointHistoryRepository, PointHistoryRepository pointHistoryRepository,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
RedisTemplate redisTemplate, RedisTemplate redisTemplate,
SearchIndexEventPublisher searchIndexEventPublisher SearchIndexEventPublisher searchIndexEventPublisher,
CategoryService categoryService
) { ) {
this.postRepository = postRepository; this.postRepository = postRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -141,6 +143,7 @@ public class PostService {
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.searchIndexEventPublisher = searchIndexEventPublisher; this.searchIndexEventPublisher = searchIndexEventPublisher;
this.categoryService = categoryService;
} }
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
@@ -429,8 +432,11 @@ public class PostService {
boolean quorumMet = totalParticipants >= cp.getQuorum(); boolean quorumMet = totalParticipants >= cp.getQuorum();
int approvePercent = totalParticipants > 0 ? (approveVotes * 100) / totalParticipants : 0; int approvePercent = totalParticipants > 0 ? (approveVotes * 100) / totalParticipants : 0;
boolean thresholdMet = approvePercent >= cp.getApproveThreshold(); boolean thresholdMet = approvePercent >= cp.getApproveThreshold();
boolean approved = false;
String rejectReason = null;
if (quorumMet && thresholdMet) { if (quorumMet && thresholdMet) {
cp.setProposalStatus(CategoryProposalStatus.APPROVED); cp.setProposalStatus(CategoryProposalStatus.APPROVED);
approved = true;
} else { } else {
cp.setProposalStatus(CategoryProposalStatus.REJECTED); cp.setProposalStatus(CategoryProposalStatus.REJECTED);
String reason; String reason;
@@ -442,6 +448,7 @@ public class PostService {
reason = "赞成率不足"; reason = "赞成率不足";
} }
cp.setRejectReason(reason); cp.setRejectReason(reason);
rejectReason = reason;
} }
cp.setResultSnapshot( cp.setResultSnapshot(
"approveVotes=" + "approveVotes=" +
@@ -452,28 +459,37 @@ public class PostService {
approvePercent approvePercent
); );
categoryProposalPostRepository.save(cp); categoryProposalPostRepository.save(cp);
if (approved) {
categoryService.createCategory(cp.getProposedName(), cp.getDescription(), "star", null);
}
if (cp.getAuthor() != null) { if (cp.getAuthor() != null) {
notificationService.createNotification( notificationService.createNotification(
cp.getAuthor(), cp.getAuthor(),
NotificationType.POLL_RESULT_OWNER, NotificationType.CATEGORY_PROPOSAL_RESULT_OWNER,
cp, cp,
null, null,
approved,
null, null,
null, null,
null, approved ? null : rejectReason
null
); );
} }
for (User participant : cp.getParticipants()) { for (User participant : cp.getParticipants()) {
if (
cp.getAuthor() != null &&
java.util.Objects.equals(participant.getId(), cp.getAuthor().getId())
) {
continue;
}
notificationService.createNotification( notificationService.createNotification(
participant, participant,
NotificationType.POLL_RESULT_PARTICIPANT, NotificationType.CATEGORY_PROPOSAL_RESULT_PARTICIPANT,
cp, cp,
null, null,
approved,
null, null,
null, null,
null, approved ? null : rejectReason
null
); );
} }
postChangeLogService.recordVoteResult(cp); postChangeLogService.recordVoteResult(cp);
@@ -576,6 +592,9 @@ public class PostService {
pollPostRepository pollPostRepository
.findById(postId) .findById(postId)
.ifPresent(pp -> { .ifPresent(pp -> {
if (pp instanceof CategoryProposalPost) {
return;
}
if (pp.isResultAnnounced()) { if (pp.isResultAnnounced()) {
return; return;
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<div class="home-page"> <div class="home-page">
<!-- <div v-if="!isMobile" class="search-container">
<div class="search-title">一切可能从此刻启航在此遇见灵感与共鸣</div>
<SearchDropdown />
</div> -->
<div class="topic-container"> <div class="topic-container">
<div class="topic-item-container"> <div class="topic-item-container">
<div <div
@@ -117,7 +112,7 @@
</div> </div>
<div v-else class="placeholder-container">分类浏览功能开发中,敬请期待。</div> <div v-else class="placeholder-container">分类浏览功能开发中,敬请期待。</div>
<!-- 通用“底部加载更多”组件(自管 loading/observer/并发) --> <!-- 通用“底部加载更多”组件(自管 loading/observer/并发) -->
<InfiniteLoadMore <InfiniteLoadMore
v-if="articles.length > 0" v-if="articles.length > 0"
:key="ioKey" :key="ioKey"

View File

@@ -75,7 +75,9 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`" :to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
> >
<span v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"></span> <span
v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"
></span>
</NuxtLink> </NuxtLink>
</span> </span>
回复了 回复了
@@ -85,7 +87,9 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
<span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span> <span
v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"
></span>
</NuxtLink> </NuxtLink>
</span> </span>
</NotificationContainer> </NotificationContainer>
@@ -115,7 +119,9 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
<span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span> <span
v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"
></span>
</NuxtLink> </NuxtLink>
</span> </span>
</NotificationContainer> </NotificationContainer>
@@ -162,7 +168,9 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
<span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span> <span
v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"
></span>
</NuxtLink> </NuxtLink>
</span> </span>
进行了表态 进行了表态
@@ -251,6 +259,38 @@
已出结果 已出结果
</NotificationContainer> </NotificationContainer>
</template> </template>
<template v-else-if="item.type === 'CATEGORY_PROPOSAL_RESULT_OWNER'">
<NotificationContainer :item="item" :markRead="markRead">
你的分类提案
<NuxtLink
class="notif-content-text"
@click="markRead(item.id)"
:to="`/posts/${item.post.id}`"
>
{{ stripMarkdownLength(item.post.title, 100) }}
</NuxtLink>
<span v-if="item.approved">已通过</span>
<span v-else>
未通过<span v-if="item.content">原因{{ item.content }}</span>
</span>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'CATEGORY_PROPOSAL_RESULT_PARTICIPANT'">
<NotificationContainer :item="item" :markRead="markRead">
你参与的分类提案
<NuxtLink
class="notif-content-text"
@click="markRead(item.id)"
:to="`/posts/${item.post.id}`"
>
{{ stripMarkdownLength(item.post.title, 100) }}
</NuxtLink>
<span v-if="item.approved">已通过</span>
<span v-else>
未通过<span v-if="item.content">原因{{ item.content }}</span>
</span>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'POST_UPDATED'"> <template v-else-if="item.type === 'POST_UPDATED'">
<NotificationContainer :item="item" :markRead="markRead"> <NotificationContainer :item="item" :markRead="markRead">
您关注的帖子 您关注的帖子
@@ -287,7 +327,9 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`" :to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
> >
<span v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"></span> <span
v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"
></span>
</NuxtLink> </NuxtLink>
回复了 回复了
<NuxtLink <NuxtLink
@@ -775,6 +817,10 @@ const formatType = (t) => {
return '发布的投票结果已公布' return '发布的投票结果已公布'
case 'POLL_RESULT_PARTICIPANT': case 'POLL_RESULT_PARTICIPANT':
return '参与的投票结果已公布' return '参与的投票结果已公布'
case 'CATEGORY_PROPOSAL_RESULT_OWNER':
return '分类提案结果已公布'
case 'CATEGORY_PROPOSAL_RESULT_PARTICIPANT':
return '参与的分类提案结果已公布'
default: default:
return t return t
} }

View File

@@ -28,6 +28,8 @@ const iconMap = {
POLL_VOTE: 'ChartHistogram', POLL_VOTE: 'ChartHistogram',
POLL_RESULT_OWNER: 'RankingList', POLL_RESULT_OWNER: 'RankingList',
POLL_RESULT_PARTICIPANT: 'ChartLine', POLL_RESULT_PARTICIPANT: 'ChartLine',
CATEGORY_PROPOSAL_RESULT_OWNER: 'TagOne',
CATEGORY_PROPOSAL_RESULT_PARTICIPANT: 'TagOne',
MENTION: 'HashtagKey', MENTION: 'HashtagKey',
POST_DELETED: 'ClearIcon', POST_DELETED: 'ClearIcon',
POST_FEATURED: 'Star', POST_FEATURED: 'Star',
@@ -254,7 +256,9 @@ function createFetchNotifications() {
} else if ( } else if (
n.type === 'POLL_VOTE' || n.type === 'POLL_VOTE' ||
n.type === 'POLL_RESULT_OWNER' || n.type === 'POLL_RESULT_OWNER' ||
n.type === 'POLL_RESULT_PARTICIPANT' n.type === 'POLL_RESULT_PARTICIPANT' ||
n.type === 'CATEGORY_PROPOSAL_RESULT_OWNER' ||
n.type === 'CATEGORY_PROPOSAL_RESULT_PARTICIPANT'
) { ) {
arr.push({ arr.push({
...n, ...n,