mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 23:51:17 +08:00
Compare commits
1 Commits
codex/rss
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a5eda311 |
@@ -171,4 +171,27 @@ public class PostController {
|
||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/featured")
|
||||
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
Authentication auth) {
|
||||
List<Long> ids = categoryIds;
|
||||
if (categoryId != null) {
|
||||
ids = java.util.List.of(categoryId);
|
||||
}
|
||||
List<Long> tids = tagIds;
|
||||
if (tagId != null) {
|
||||
tids = java.util.List.of(tagId);
|
||||
}
|
||||
if (auth != null) {
|
||||
userVisitService.recordVisit(auth.getName());
|
||||
}
|
||||
return postService.listFeaturedPosts(ids, tids, page, pageSize)
|
||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
12
backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java
Normal file
12
backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class FeaturedMedalDto extends MedalDto {
|
||||
private long currentFeaturedCount;
|
||||
private long targetFeaturedCount;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.openisle.model;
|
||||
public enum MedalType {
|
||||
COMMENT,
|
||||
POST,
|
||||
FEATURED,
|
||||
CONTRIBUTOR,
|
||||
SEED,
|
||||
PIONEER
|
||||
|
||||
@@ -40,6 +40,8 @@ public enum NotificationType {
|
||||
LOTTERY_WIN,
|
||||
/** Your lottery post was drawn */
|
||||
LOTTERY_DRAW,
|
||||
/** Your post was featured */
|
||||
POST_FEATURED,
|
||||
/** You were mentioned in a post or comment */
|
||||
MENTION
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public enum PointHistoryType {
|
||||
POST_LIKED,
|
||||
COMMENT_LIKED,
|
||||
INVITE,
|
||||
FEATURE,
|
||||
SYSTEM_ONLINE,
|
||||
REDEEM
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
|
||||
long countDistinctByTags_Id(Long tagId);
|
||||
|
||||
long countByAuthor_IdAndRssExcludedFalse(Long userId);
|
||||
|
||||
@Query("SELECT t.id, COUNT(DISTINCT p) FROM Post p JOIN p.tags t WHERE t.id IN :tagIds GROUP BY t.id")
|
||||
List<Object[]> countPostsByTagIds(@Param("tagIds") List<Long> tagIds);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.openisle.dto.MedalDto;
|
||||
import com.openisle.dto.PostMedalDto;
|
||||
import com.openisle.dto.SeedUserMedalDto;
|
||||
import com.openisle.dto.PioneerMedalDto;
|
||||
import com.openisle.dto.FeaturedMedalDto;
|
||||
import com.openisle.model.MedalType;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.CommentRepository;
|
||||
@@ -74,6 +75,23 @@ public class MedalService {
|
||||
postMedal.setSelected(selected == MedalType.POST);
|
||||
medals.add(postMedal);
|
||||
|
||||
FeaturedMedalDto featuredMedal = new FeaturedMedalDto();
|
||||
featuredMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_rss.png");
|
||||
featuredMedal.setTitle("精选作者");
|
||||
featuredMedal.setDescription("至少有1篇文章被收录为精选");
|
||||
featuredMedal.setType(MedalType.FEATURED);
|
||||
featuredMedal.setTargetFeaturedCount(1);
|
||||
if (user != null) {
|
||||
long count = postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId());
|
||||
featuredMedal.setCurrentFeaturedCount(count);
|
||||
featuredMedal.setCompleted(count >= 1);
|
||||
} else {
|
||||
featuredMedal.setCurrentFeaturedCount(0);
|
||||
featuredMedal.setCompleted(false);
|
||||
}
|
||||
featuredMedal.setSelected(selected == MedalType.FEATURED);
|
||||
medals.add(featuredMedal);
|
||||
|
||||
ContributorMedalDto contributorMedal = new ContributorMedalDto();
|
||||
contributorMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png");
|
||||
contributorMedal.setTitle("贡献者");
|
||||
@@ -141,6 +159,8 @@ public class MedalService {
|
||||
user.setDisplayMedal(MedalType.COMMENT);
|
||||
} else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) {
|
||||
user.setDisplayMedal(MedalType.POST);
|
||||
} else if (postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()) >= 1) {
|
||||
user.setDisplayMedal(MedalType.FEATURED);
|
||||
} else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) {
|
||||
user.setDisplayMedal(MedalType.CONTRIBUTOR);
|
||||
} else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) {
|
||||
|
||||
@@ -33,6 +33,12 @@ public class PointService {
|
||||
return addPoint(user, 500, PointHistoryType.INVITE, null, null, invitee);
|
||||
}
|
||||
|
||||
public int awardForFeatured(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||
}
|
||||
|
||||
private PointLog getTodayLog(User user) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||
|
||||
@@ -67,6 +67,7 @@ public class PostService {
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final EmailSender emailSender;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final PointService pointService;
|
||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
private String websiteUrl;
|
||||
@@ -89,6 +90,7 @@ public class PostService {
|
||||
TaskScheduler taskScheduler,
|
||||
EmailSender emailSender,
|
||||
ApplicationContext applicationContext,
|
||||
PointService pointService,
|
||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
||||
this.postRepository = postRepository;
|
||||
this.userRepository = userRepository;
|
||||
@@ -107,6 +109,7 @@ public class PostService {
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.emailSender = emailSender;
|
||||
this.applicationContext = applicationContext;
|
||||
this.pointService = pointService;
|
||||
this.publishMode = publishMode;
|
||||
}
|
||||
|
||||
@@ -146,7 +149,10 @@ public class PostService {
|
||||
public Post includeInRss(Long id) {
|
||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||
post.setRssExcluded(false);
|
||||
return postRepository.save(post);
|
||||
post = postRepository.save(post);
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
|
||||
pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
|
||||
return post;
|
||||
}
|
||||
|
||||
public Post createPost(String username,
|
||||
@@ -458,6 +464,26 @@ public class PostService {
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
public List<Post> listFeaturedPosts(java.util.List<Long> categoryIds,
|
||||
java.util.List<Long> tagIds,
|
||||
Integer page,
|
||||
Integer pageSize) {
|
||||
List<Post> posts;
|
||||
boolean hasCategories = categoryIds != null && !categoryIds.isEmpty();
|
||||
boolean hasTags = tagIds != null && !tagIds.isEmpty();
|
||||
if (hasCategories && hasTags) {
|
||||
posts = listPostsByCategoriesAndTags(categoryIds, tagIds, null, null);
|
||||
} else if (hasCategories) {
|
||||
posts = listPostsByCategories(categoryIds, null, null);
|
||||
} else if (hasTags) {
|
||||
posts = listPostsByTags(tagIds, null, null);
|
||||
} else {
|
||||
posts = listPosts();
|
||||
}
|
||||
posts = posts.stream().filter(p -> !Boolean.TRUE.equals(p.getRssExcluded())).toList();
|
||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
||||
}
|
||||
|
||||
public List<Post> listPendingPosts() {
|
||||
return postRepository.findByStatus(PostStatus.PENDING);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class MedalServiceTest {
|
||||
|
||||
List<MedalDto> medals = service.getMedals(null);
|
||||
medals.forEach(m -> assertFalse(m.isCompleted()));
|
||||
assertEquals(5, medals.size());
|
||||
assertEquals(6, medals.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<template v-else-if="medal.type === 'POST'">
|
||||
{{ medal.currentPostCount }}/{{ medal.targetPostCount }}
|
||||
</template>
|
||||
<template v-else-if="medal.type === 'FEATURED'">
|
||||
{{ medal.currentFeaturedCount }}/{{ medal.targetFeaturedCount }}
|
||||
</template>
|
||||
<template v-else-if="medal.type === 'CONTRIBUTOR'">
|
||||
{{ medal.currentContributionLines }}/{{ medal.targetContributionLines }}
|
||||
</template>
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
<div class="article-container">
|
||||
<template
|
||||
v-if="
|
||||
selectedTopic === '最新' || selectedTopic === '排行榜' || selectedTopic === '最新回复'
|
||||
selectedTopic === '最新' ||
|
||||
selectedTopic === '排行榜' ||
|
||||
selectedTopic === '最新回复' ||
|
||||
selectedTopic === '精选'
|
||||
"
|
||||
>
|
||||
<div class="article-header-container">
|
||||
@@ -152,7 +155,7 @@ const route = useRoute()
|
||||
const tagOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
|
||||
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
||||
const topics = ref(['精选', '最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
||||
const selectedTopicCookie = useCookie('homeTab')
|
||||
const selectedTopic = ref(
|
||||
selectedTopicCookie.value
|
||||
@@ -236,6 +239,7 @@ const baseQuery = computed(() => ({
|
||||
const listApiPath = computed(() => {
|
||||
if (selectedTopic.value === '排行榜') return '/api/posts/ranking'
|
||||
if (selectedTopic.value === '最新回复') return '/api/posts/latest-reply'
|
||||
if (selectedTopic.value === '精选') return '/api/posts/featured'
|
||||
return '/api/posts'
|
||||
})
|
||||
const buildUrl = ({ pageNo }) => {
|
||||
|
||||
@@ -493,6 +493,19 @@
|
||||
已被管理员拒绝
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'POST_FEATURED'">
|
||||
<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>
|
||||
被收录为精选
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'POST_DELETED'">
|
||||
<NotificationContainer :item="item" :markRead="markRead">
|
||||
管理员
|
||||
@@ -674,6 +687,8 @@ const formatType = (t) => {
|
||||
return '抽奖已开奖'
|
||||
case 'POST_DELETED':
|
||||
return '帖子被删除'
|
||||
case 'POST_FEATURED':
|
||||
return '文章被精选'
|
||||
default:
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -136,6 +136,13 @@
|
||||
}}</NuxtLink>
|
||||
加入社区 🎉,获得 {{ item.amount }} 积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'FEATURE'">
|
||||
文章
|
||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||
item.postTitle
|
||||
}}</NuxtLink>
|
||||
被收录为精选,获得 {{ item.amount }} 积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'REDEEM'">
|
||||
兑换商品,消耗 {{ -item.amount }} 积分
|
||||
</template>
|
||||
@@ -176,6 +183,7 @@ const pointRules = [
|
||||
'帖子被点赞:每次 10 积分',
|
||||
'评论被点赞:每次 10 积分',
|
||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
||||
'文章被收录至精选:每次 500 积分',
|
||||
]
|
||||
|
||||
const goods = ref([])
|
||||
@@ -192,6 +200,7 @@ const iconMap = {
|
||||
INVITE: 'fas fa-user-plus',
|
||||
SYSTEM_ONLINE: 'fas fa-clock',
|
||||
REDEEM: 'fas fa-gift',
|
||||
FEATURE: 'fas fa-star',
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const medalTitles = {
|
||||
COMMENT: '评论达人',
|
||||
POST: '发帖达人',
|
||||
FEATURED: '精选作者',
|
||||
SEED: '种子用户',
|
||||
CONTRIBUTOR: '贡献者',
|
||||
PIONEER: '开山鼻祖',
|
||||
|
||||
@@ -27,6 +27,7 @@ const iconMap = {
|
||||
LOTTERY_DRAW: 'fas fa-bullhorn',
|
||||
MENTION: 'fas fa-at',
|
||||
POST_DELETED: 'fas fa-trash',
|
||||
POST_FEATURED: 'fas fa-star',
|
||||
}
|
||||
|
||||
export async function fetchUnreadCount() {
|
||||
@@ -267,6 +268,17 @@ function createFetchNotifications() {
|
||||
}
|
||||
},
|
||||
})
|
||||
} else if (n.type === 'POST_FEATURED') {
|
||||
arr.push({
|
||||
...n,
|
||||
icon: iconMap[n.type],
|
||||
iconClick: () => {
|
||||
if (n.post) {
|
||||
markNotificationRead(n.id)
|
||||
navigateTo(`/posts/${n.post.id}`, { replace: true })
|
||||
}
|
||||
},
|
||||
})
|
||||
} else if (n.type === 'REGISTER_REQUEST') {
|
||||
arr.push({
|
||||
...n,
|
||||
|
||||
Reference in New Issue
Block a user