mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-13 18:40:58 +08:00
Compare commits
6 Commits
codex/rss
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3dcc98122 | ||
|
|
c648d4cf39 | ||
|
|
41a5eda311 | ||
|
|
c6e0dc6a1d | ||
|
|
92e630df22 | ||
|
|
c6b0f32b09 |
@@ -171,4 +171,27 @@ public class PostController {
|
|||||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
||||||
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
|
.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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,18 +107,11 @@ public class RssController {
|
|||||||
enclosure = absolutifyUrl(enclosure, base);
|
enclosure = absolutifyUrl(enclosure, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top comments in Markdown
|
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
||||||
List<Comment> topComments = commentService
|
List<Comment> topComments = commentService
|
||||||
.getCommentsForPost(p.getId(), CommentSort.MOST_INTERACTIONS);
|
.getCommentsForPost(p.getId(), CommentSort.MOST_INTERACTIONS);
|
||||||
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
||||||
StringBuilder commentMd = new StringBuilder();
|
String footerHtml = buildFooterHtml(base, link, topComments);
|
||||||
for (Comment c : topComments) {
|
|
||||||
commentMd.append("> @")
|
|
||||||
.append(nullSafe(c.getAuthor().getUsername()))
|
|
||||||
.append(": ")
|
|
||||||
.append(nullSafe(c.getContent()).replace("\r", ""))
|
|
||||||
.append("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append("<item>");
|
sb.append("<item>");
|
||||||
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
||||||
@@ -127,18 +120,16 @@ public class RssController {
|
|||||||
elem(sb, "pubDate", toRfc1123Gmt(p.getCreatedAt().atZone(ZoneId.systemDefault())));
|
elem(sb, "pubDate", toRfc1123Gmt(p.getCreatedAt().atZone(ZoneId.systemDefault())));
|
||||||
// 摘要
|
// 摘要
|
||||||
elem(sb, "description", cdata(plain));
|
elem(sb, "description", cdata(plain));
|
||||||
// 全文(HTML)
|
// 全文(HTML):正文 + 优雅的 Markdown 区块(已转 HTML)
|
||||||
sb.append("<content:encoded><![CDATA[").append(absHtml).append("]]></content:encoded>");
|
sb.append("<content:encoded><![CDATA[")
|
||||||
|
.append(absHtml)
|
||||||
|
.append(footerHtml)
|
||||||
|
.append("]]></content:encoded>");
|
||||||
// 首图 enclosure(图片类型)
|
// 首图 enclosure(图片类型)
|
||||||
if (enclosure != null) {
|
if (enclosure != null) {
|
||||||
sb.append("<enclosure url=\"").append(escapeXml(enclosure)).append("\" type=\"")
|
sb.append("<enclosure url=\"").append(escapeXml(enclosure)).append("\" type=\"")
|
||||||
.append(getMimeType(enclosure)).append("\" />");
|
.append(getMimeType(enclosure)).append("\" />");
|
||||||
}
|
}
|
||||||
// Markdown comments
|
|
||||||
elem(sb, "commentsMarkdown", cdata(commentMd.toString()));
|
|
||||||
// Markdown original link
|
|
||||||
elem(sb, "originalLinkMarkdown", cdata("[原文链接](" + link + ")"));
|
|
||||||
|
|
||||||
sb.append("</item>");
|
sb.append("</item>");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +149,12 @@ public class RssController {
|
|||||||
private static String sanitizeHtml(String html) {
|
private static String sanitizeHtml(String html) {
|
||||||
if (html == null) return "";
|
if (html == null) return "";
|
||||||
Safelist wl = Safelist.relaxed()
|
Safelist wl = Safelist.relaxed()
|
||||||
.addTags("pre", "code", "figure", "figcaption", "picture", "source",
|
.addTags(
|
||||||
"table","thead","tbody","tr","th","td","h1","h2","h3","h4","h5","h6")
|
"pre","code","figure","figcaption","picture","source",
|
||||||
|
"table","thead","tbody","tr","th","td",
|
||||||
|
"h1","h2","h3","h4","h5","h6",
|
||||||
|
"hr","blockquote"
|
||||||
|
)
|
||||||
.addAttributes("a", "href", "title", "target", "rel")
|
.addAttributes("a", "href", "title", "target", "rel")
|
||||||
.addAttributes("img", "src", "alt", "title", "width", "height")
|
.addAttributes("img", "src", "alt", "title", "width", "height")
|
||||||
.addAttributes("source", "srcset", "type", "media")
|
.addAttributes("source", "srcset", "type", "media")
|
||||||
@@ -268,6 +263,59 @@ public class RssController {
|
|||||||
return "image/jpeg";
|
return "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== 附加区块(原文链接 + 精选评论) ===================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将“原文链接 + 精选评论(最多 10 条)”以优雅的 Markdown 形式渲染为 HTML,
|
||||||
|
* 并做 sanitize + 绝对化,然后拼入 content:encoded 尾部。
|
||||||
|
*/
|
||||||
|
private static String buildFooterHtml(String baseUrl, String originalLink, List<Comment> topComments) {
|
||||||
|
StringBuilder md = new StringBuilder(256);
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
md.append("\n\n---\n\n");
|
||||||
|
|
||||||
|
// 原文链接(强调 + 可点击)
|
||||||
|
md.append("**原文链接:** ")
|
||||||
|
.append("[").append(originalLink).append("](").append(originalLink).append(")")
|
||||||
|
.append("\n\n");
|
||||||
|
|
||||||
|
// 精选评论(仅当有评论时展示)
|
||||||
|
if (topComments != null && !topComments.isEmpty()) {
|
||||||
|
md.append("### 精选评论(Top ").append(Math.min(10, topComments.size())).append(")\n\n");
|
||||||
|
for (Comment c : topComments) {
|
||||||
|
String author = usernameOf(c);
|
||||||
|
String content = nullSafe(c.getContent()).replace("\r", "");
|
||||||
|
// 使用引用样式展示,提升可读性
|
||||||
|
md.append("> @").append(author).append(": ").append(content).append("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染为 HTML,并保持和正文一致的处理流程
|
||||||
|
String html = renderMarkdown(md.toString());
|
||||||
|
String safe = sanitizeHtml(html);
|
||||||
|
return absolutifyHtml(safe, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String usernameOf(Comment c) {
|
||||||
|
if (c == null) return "匿名";
|
||||||
|
try {
|
||||||
|
Object authorObj = c.getAuthor();
|
||||||
|
if (authorObj == null) return "匿名";
|
||||||
|
// 反射避免直接依赖实体字段名变化(也可直接强转到具体类型)
|
||||||
|
String username;
|
||||||
|
try {
|
||||||
|
username = (String) authorObj.getClass().getMethod("getUsername").invoke(authorObj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
username = null;
|
||||||
|
}
|
||||||
|
if (username == null || username.isEmpty()) return "匿名";
|
||||||
|
return username;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "匿名";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== 时间/字符串/XML ===================== */
|
/* ===================== 时间/字符串/XML ===================== */
|
||||||
|
|
||||||
private static String toRfc1123Gmt(ZonedDateTime zdt) {
|
private static String toRfc1123Gmt(ZonedDateTime zdt) {
|
||||||
|
|||||||
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 {
|
public enum MedalType {
|
||||||
COMMENT,
|
COMMENT,
|
||||||
POST,
|
POST,
|
||||||
|
FEATURED,
|
||||||
CONTRIBUTOR,
|
CONTRIBUTOR,
|
||||||
SEED,
|
SEED,
|
||||||
PIONEER
|
PIONEER
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public enum NotificationType {
|
|||||||
LOTTERY_WIN,
|
LOTTERY_WIN,
|
||||||
/** Your lottery post was drawn */
|
/** Your lottery post was drawn */
|
||||||
LOTTERY_DRAW,
|
LOTTERY_DRAW,
|
||||||
|
/** Your post was featured */
|
||||||
|
POST_FEATURED,
|
||||||
/** You were mentioned in a post or comment */
|
/** You were mentioned in a post or comment */
|
||||||
MENTION
|
MENTION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public enum PointHistoryType {
|
|||||||
POST_LIKED,
|
POST_LIKED,
|
||||||
COMMENT_LIKED,
|
COMMENT_LIKED,
|
||||||
INVITE,
|
INVITE,
|
||||||
|
FEATURE,
|
||||||
SYSTEM_ONLINE,
|
SYSTEM_ONLINE,
|
||||||
REDEEM
|
REDEEM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ public interface PostRepository extends JpaRepository<Post, Long> {
|
|||||||
|
|
||||||
long countDistinctByTags_Id(Long tagId);
|
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")
|
@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);
|
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.PostMedalDto;
|
||||||
import com.openisle.dto.SeedUserMedalDto;
|
import com.openisle.dto.SeedUserMedalDto;
|
||||||
import com.openisle.dto.PioneerMedalDto;
|
import com.openisle.dto.PioneerMedalDto;
|
||||||
|
import com.openisle.dto.FeaturedMedalDto;
|
||||||
import com.openisle.model.MedalType;
|
import com.openisle.model.MedalType;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.CommentRepository;
|
import com.openisle.repository.CommentRepository;
|
||||||
@@ -74,6 +75,23 @@ public class MedalService {
|
|||||||
postMedal.setSelected(selected == MedalType.POST);
|
postMedal.setSelected(selected == MedalType.POST);
|
||||||
medals.add(postMedal);
|
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();
|
ContributorMedalDto contributorMedal = new ContributorMedalDto();
|
||||||
contributorMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png");
|
contributorMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png");
|
||||||
contributorMedal.setTitle("贡献者");
|
contributorMedal.setTitle("贡献者");
|
||||||
@@ -141,6 +159,8 @@ public class MedalService {
|
|||||||
user.setDisplayMedal(MedalType.COMMENT);
|
user.setDisplayMedal(MedalType.COMMENT);
|
||||||
} else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) {
|
} else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) {
|
||||||
user.setDisplayMedal(MedalType.POST);
|
user.setDisplayMedal(MedalType.POST);
|
||||||
|
} else if (postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()) >= 1) {
|
||||||
|
user.setDisplayMedal(MedalType.FEATURED);
|
||||||
} else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) {
|
} else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) {
|
||||||
user.setDisplayMedal(MedalType.CONTRIBUTOR);
|
user.setDisplayMedal(MedalType.CONTRIBUTOR);
|
||||||
} else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) {
|
} else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public class PointService {
|
|||||||
return addPoint(user, 500, PointHistoryType.INVITE, null, null, invitee);
|
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) {
|
private PointLog getTodayLog(User user) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ public class PostService {
|
|||||||
private final TaskScheduler taskScheduler;
|
private final TaskScheduler taskScheduler;
|
||||||
private final EmailSender emailSender;
|
private final EmailSender emailSender;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
|
private final PointService pointService;
|
||||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
@@ -89,6 +90,7 @@ public class PostService {
|
|||||||
TaskScheduler taskScheduler,
|
TaskScheduler taskScheduler,
|
||||||
EmailSender emailSender,
|
EmailSender emailSender,
|
||||||
ApplicationContext applicationContext,
|
ApplicationContext applicationContext,
|
||||||
|
PointService pointService,
|
||||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
||||||
this.postRepository = postRepository;
|
this.postRepository = postRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
@@ -107,6 +109,7 @@ public class PostService {
|
|||||||
this.taskScheduler = taskScheduler;
|
this.taskScheduler = taskScheduler;
|
||||||
this.emailSender = emailSender;
|
this.emailSender = emailSender;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
|
this.pointService = pointService;
|
||||||
this.publishMode = publishMode;
|
this.publishMode = publishMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +149,10 @@ public class PostService {
|
|||||||
public Post includeInRss(Long id) {
|
public Post includeInRss(Long id) {
|
||||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
post.setRssExcluded(false);
|
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,
|
public Post createPost(String username,
|
||||||
@@ -458,6 +464,26 @@ public class PostService {
|
|||||||
return paginate(sortByPinnedAndCreated(posts), page, pageSize);
|
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() {
|
public List<Post> listPendingPosts() {
|
||||||
return postRepository.findByStatus(PostStatus.PENDING);
|
return postRepository.findByStatus(PostStatus.PENDING);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class MedalServiceTest {
|
|||||||
|
|
||||||
List<MedalDto> medals = service.getMedals(null);
|
List<MedalDto> medals = service.getMedals(null);
|
||||||
medals.forEach(m -> assertFalse(m.isCompleted()));
|
medals.forEach(m -> assertFalse(m.isCompleted()));
|
||||||
assertEquals(5, medals.size());
|
assertEquals(6, medals.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ class PostServiceTest {
|
|||||||
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.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,
|
notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -80,11 +81,12 @@ class PostServiceTest {
|
|||||||
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.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,
|
notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -132,11 +134,12 @@ class PostServiceTest {
|
|||||||
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.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,
|
notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
||||||
@@ -165,11 +168,12 @@ class PostServiceTest {
|
|||||||
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
TaskScheduler taskScheduler = mock(TaskScheduler.class);
|
||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.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,
|
notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
User author = new User();
|
User author = new User();
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
<template v-else-if="medal.type === 'POST'">
|
<template v-else-if="medal.type === 'POST'">
|
||||||
{{ medal.currentPostCount }}/{{ medal.targetPostCount }}
|
{{ medal.currentPostCount }}/{{ medal.targetPostCount }}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="medal.type === 'FEATURED'">
|
||||||
|
{{ medal.currentFeaturedCount }}/{{ medal.targetFeaturedCount }}
|
||||||
|
</template>
|
||||||
<template v-else-if="medal.type === 'CONTRIBUTOR'">
|
<template v-else-if="medal.type === 'CONTRIBUTOR'">
|
||||||
{{ medal.currentContributionLines }}/{{ medal.targetContributionLines }}
|
{{ medal.currentContributionLines }}/{{ medal.targetContributionLines }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,7 +26,10 @@
|
|||||||
<div class="article-container">
|
<div class="article-container">
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
selectedTopic === '最新' || selectedTopic === '排行榜' || selectedTopic === '最新回复'
|
selectedTopic === '最新' ||
|
||||||
|
selectedTopic === '排行榜' ||
|
||||||
|
selectedTopic === '最新回复' ||
|
||||||
|
selectedTopic === '精选'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="article-header-container">
|
<div class="article-header-container">
|
||||||
@@ -152,7 +155,7 @@ const route = useRoute()
|
|||||||
const tagOptions = ref([])
|
const tagOptions = ref([])
|
||||||
const categoryOptions = ref([])
|
const categoryOptions = ref([])
|
||||||
|
|
||||||
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
const topics = ref(['精选', '最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
||||||
const selectedTopicCookie = useCookie('homeTab')
|
const selectedTopicCookie = useCookie('homeTab')
|
||||||
const selectedTopic = ref(
|
const selectedTopic = ref(
|
||||||
selectedTopicCookie.value
|
selectedTopicCookie.value
|
||||||
@@ -236,6 +239,7 @@ const baseQuery = computed(() => ({
|
|||||||
const listApiPath = computed(() => {
|
const listApiPath = computed(() => {
|
||||||
if (selectedTopic.value === '排行榜') return '/api/posts/ranking'
|
if (selectedTopic.value === '排行榜') return '/api/posts/ranking'
|
||||||
if (selectedTopic.value === '最新回复') return '/api/posts/latest-reply'
|
if (selectedTopic.value === '最新回复') return '/api/posts/latest-reply'
|
||||||
|
if (selectedTopic.value === '精选') return '/api/posts/featured'
|
||||||
return '/api/posts'
|
return '/api/posts'
|
||||||
})
|
})
|
||||||
const buildUrl = ({ pageNo }) => {
|
const buildUrl = ({ pageNo }) => {
|
||||||
|
|||||||
@@ -493,6 +493,19 @@
|
|||||||
已被管理员拒绝
|
已被管理员拒绝
|
||||||
</NotificationContainer>
|
</NotificationContainer>
|
||||||
</template>
|
</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'">
|
<template v-else-if="item.type === 'POST_DELETED'">
|
||||||
<NotificationContainer :item="item" :markRead="markRead">
|
<NotificationContainer :item="item" :markRead="markRead">
|
||||||
管理员
|
管理员
|
||||||
@@ -674,6 +687,8 @@ const formatType = (t) => {
|
|||||||
return '抽奖已开奖'
|
return '抽奖已开奖'
|
||||||
case 'POST_DELETED':
|
case 'POST_DELETED':
|
||||||
return '帖子被删除'
|
return '帖子被删除'
|
||||||
|
case 'POST_FEATURED':
|
||||||
|
return '文章被精选'
|
||||||
default:
|
default:
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,13 @@
|
|||||||
}}</NuxtLink>
|
}}</NuxtLink>
|
||||||
加入社区 🎉,获得 {{ item.amount }} 积分
|
加入社区 🎉,获得 {{ item.amount }} 积分
|
||||||
</template>
|
</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'">
|
<template v-else-if="item.type === 'REDEEM'">
|
||||||
兑换商品,消耗 {{ -item.amount }} 积分
|
兑换商品,消耗 {{ -item.amount }} 积分
|
||||||
</template>
|
</template>
|
||||||
@@ -176,6 +183,7 @@ const pointRules = [
|
|||||||
'帖子被点赞:每次 10 积分',
|
'帖子被点赞:每次 10 积分',
|
||||||
'评论被点赞:每次 10 积分',
|
'评论被点赞:每次 10 积分',
|
||||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
||||||
|
'文章被收录至精选:每次 500 积分',
|
||||||
]
|
]
|
||||||
|
|
||||||
const goods = ref([])
|
const goods = ref([])
|
||||||
@@ -192,6 +200,7 @@ const iconMap = {
|
|||||||
INVITE: 'fas fa-user-plus',
|
INVITE: 'fas fa-user-plus',
|
||||||
SYSTEM_ONLINE: 'fas fa-clock',
|
SYSTEM_ONLINE: 'fas fa-clock',
|
||||||
REDEEM: 'fas fa-gift',
|
REDEEM: 'fas fa-gift',
|
||||||
|
FEATURE: 'fas fa-star',
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const medalTitles = {
|
export const medalTitles = {
|
||||||
COMMENT: '评论达人',
|
COMMENT: '评论达人',
|
||||||
POST: '发帖达人',
|
POST: '发帖达人',
|
||||||
|
FEATURED: '精选作者',
|
||||||
SEED: '种子用户',
|
SEED: '种子用户',
|
||||||
CONTRIBUTOR: '贡献者',
|
CONTRIBUTOR: '贡献者',
|
||||||
PIONEER: '开山鼻祖',
|
PIONEER: '开山鼻祖',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const iconMap = {
|
|||||||
LOTTERY_DRAW: 'fas fa-bullhorn',
|
LOTTERY_DRAW: 'fas fa-bullhorn',
|
||||||
MENTION: 'fas fa-at',
|
MENTION: 'fas fa-at',
|
||||||
POST_DELETED: 'fas fa-trash',
|
POST_DELETED: 'fas fa-trash',
|
||||||
|
POST_FEATURED: 'fas fa-star',
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUnreadCount() {
|
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') {
|
} else if (n.type === 'REGISTER_REQUEST') {
|
||||||
arr.push({
|
arr.push({
|
||||||
...n,
|
...n,
|
||||||
|
|||||||
Reference in New Issue
Block a user