diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 86d8fd9d1..3ea7334e2 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -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 featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "categoryIds", required = false) List categoryIds, + @RequestParam(value = "tagId", required = false) Long tagId, + @RequestParam(value = "tagIds", required = false) List tagIds, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "pageSize", required = false) Integer pageSize, + Authentication auth) { + List ids = categoryIds; + if (categoryId != null) { + ids = java.util.List.of(categoryId); + } + List 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()); + } } diff --git a/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java b/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java new file mode 100644 index 000000000..2e5cbaf9e --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java @@ -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; +} + diff --git a/backend/src/main/java/com/openisle/model/MedalType.java b/backend/src/main/java/com/openisle/model/MedalType.java index 00553511d..c6509cebb 100644 --- a/backend/src/main/java/com/openisle/model/MedalType.java +++ b/backend/src/main/java/com/openisle/model/MedalType.java @@ -3,6 +3,7 @@ package com.openisle.model; public enum MedalType { COMMENT, POST, + FEATURED, CONTRIBUTOR, SEED, PIONEER diff --git a/backend/src/main/java/com/openisle/model/NotificationType.java b/backend/src/main/java/com/openisle/model/NotificationType.java index d8e3a99a1..c4b4e0e25 100644 --- a/backend/src/main/java/com/openisle/model/NotificationType.java +++ b/backend/src/main/java/com/openisle/model/NotificationType.java @@ -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 } diff --git a/backend/src/main/java/com/openisle/model/PointHistoryType.java b/backend/src/main/java/com/openisle/model/PointHistoryType.java index 7e71b1bef..af03d989c 100644 --- a/backend/src/main/java/com/openisle/model/PointHistoryType.java +++ b/backend/src/main/java/com/openisle/model/PointHistoryType.java @@ -6,6 +6,7 @@ public enum PointHistoryType { POST_LIKED, COMMENT_LIKED, INVITE, + FEATURE, SYSTEM_ONLINE, REDEEM } diff --git a/backend/src/main/java/com/openisle/repository/PostRepository.java b/backend/src/main/java/com/openisle/repository/PostRepository.java index 58083b193..a072c83f1 100644 --- a/backend/src/main/java/com/openisle/repository/PostRepository.java +++ b/backend/src/main/java/com/openisle/repository/PostRepository.java @@ -97,6 +97,8 @@ public interface PostRepository extends JpaRepository { 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 countPostsByTagIds(@Param("tagIds") List tagIds); diff --git a/backend/src/main/java/com/openisle/service/MedalService.java b/backend/src/main/java/com/openisle/service/MedalService.java index daa4acc31..1f43caccd 100644 --- a/backend/src/main/java/com/openisle/service/MedalService.java +++ b/backend/src/main/java/com/openisle/service/MedalService.java @@ -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) { diff --git a/backend/src/main/java/com/openisle/service/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index 69a543389..2b5a53060 100644 --- a/backend/src/main/java/com/openisle/service/PointService.java +++ b/backend/src/main/java/com/openisle/service/PointService.java @@ -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) diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index f0238d29e..d62a7fa40 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -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> 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 listFeaturedPosts(java.util.List categoryIds, + java.util.List tagIds, + Integer page, + Integer pageSize) { + List 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 listPendingPosts() { return postRepository.findByStatus(PostStatus.PENDING); } diff --git a/backend/src/test/java/com/openisle/service/MedalServiceTest.java b/backend/src/test/java/com/openisle/service/MedalServiceTest.java index a4a10a56d..a873ea9c6 100644 --- a/backend/src/test/java/com/openisle/service/MedalServiceTest.java +++ b/backend/src/test/java/com/openisle/service/MedalServiceTest.java @@ -27,7 +27,7 @@ class MedalServiceTest { List medals = service.getMedals(null); medals.forEach(m -> assertFalse(m.isCompleted())); - assertEquals(5, medals.size()); + assertEquals(6, medals.size()); } @Test diff --git a/frontend_nuxt/components/AchievementList.vue b/frontend_nuxt/components/AchievementList.vue index a58e4b4a2..ddcceef94 100644 --- a/frontend_nuxt/components/AchievementList.vue +++ b/frontend_nuxt/components/AchievementList.vue @@ -26,6 +26,9 @@ + diff --git a/frontend_nuxt/pages/index.vue b/frontend_nuxt/pages/index.vue index fd0434dc4..d99bff85a 100644 --- a/frontend_nuxt/pages/index.vue +++ b/frontend_nuxt/pages/index.vue @@ -26,7 +26,10 @@
+ + @@ -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 () => { diff --git a/frontend_nuxt/utils/medal.js b/frontend_nuxt/utils/medal.js index 2f61e537d..efc8ea1f8 100644 --- a/frontend_nuxt/utils/medal.js +++ b/frontend_nuxt/utils/medal.js @@ -1,6 +1,7 @@ export const medalTitles = { COMMENT: '评论达人', POST: '发帖达人', + FEATURED: '精选作者', SEED: '种子用户', CONTRIBUTOR: '贡献者', PIONEER: '开山鼻祖', diff --git a/frontend_nuxt/utils/notification.js b/frontend_nuxt/utils/notification.js index e476d9fb1..2d3fcc410 100644 --- a/frontend_nuxt/utils/notification.js +++ b/frontend_nuxt/utils/notification.js @@ -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,