mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-23 14:40:49 +08:00
Compare commits
18 Commits
codex-5yja
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3dcc98122 | ||
|
|
c648d4cf39 | ||
|
|
41a5eda311 | ||
|
|
c6e0dc6a1d | ||
|
|
92e630df22 | ||
|
|
c6b0f32b09 | ||
|
|
5f5b6f84a8 | ||
|
|
cd57d478f2 | ||
|
|
da07313df8 | ||
|
|
c08ecb5e33 | ||
|
|
0a722c81c5 | ||
|
|
15071471b2 | ||
|
|
98a9939738 | ||
|
|
72e9a77373 | ||
|
|
ed7dcd9414 | ||
|
|
79fe8b5997 | ||
|
|
cfce4d7d1d | ||
|
|
b7f5d8485c |
@@ -47,13 +47,14 @@ public class AuthController {
|
|||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||||
}
|
}
|
||||||
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
|
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
|
||||||
if (!inviteService.validate(req.getInviteToken())) {
|
InviteService.InviteValidateResult result = inviteService.validate(req.getInviteToken());
|
||||||
|
if (!result.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
|
return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
User user = userService.registerWithInvite(
|
User user = userService.registerWithInvite(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword());
|
req.getUsername(), req.getEmail(), req.getPassword());
|
||||||
inviteService.consume(req.getInviteToken());
|
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
"token", jwtService.generateToken(user.getUsername()),
|
||||||
@@ -144,7 +145,8 @@ public class AuthController {
|
|||||||
@PostMapping("/google")
|
@PostMapping("/google")
|
||||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
||||||
@@ -154,7 +156,7 @@ public class AuthController {
|
|||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken());
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
"reason_code", "INVITE_APPROVED"
|
"reason_code", "INVITE_APPROVED"
|
||||||
@@ -218,7 +220,8 @@ public class AuthController {
|
|||||||
@PostMapping("/github")
|
@PostMapping("/github")
|
||||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = githubAuthService.authenticate(
|
Optional<AuthResult> resultOpt = githubAuthService.authenticate(
|
||||||
@@ -229,7 +232,7 @@ public class AuthController {
|
|||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken());
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
"reason_code", "INVITE_APPROVED"
|
"reason_code", "INVITE_APPROVED"
|
||||||
@@ -265,7 +268,8 @@ public class AuthController {
|
|||||||
@PostMapping("/discord")
|
@PostMapping("/discord")
|
||||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = discordAuthService.authenticate(
|
Optional<AuthResult> resultOpt = discordAuthService.authenticate(
|
||||||
@@ -276,7 +280,7 @@ public class AuthController {
|
|||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken());
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
"reason_code", "INVITE_APPROVED"
|
"reason_code", "INVITE_APPROVED"
|
||||||
@@ -311,7 +315,8 @@ public class AuthController {
|
|||||||
@PostMapping("/twitter")
|
@PostMapping("/twitter")
|
||||||
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
||||||
|
if (viaInvite && !inviteValidateResult.isValidate()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
||||||
}
|
}
|
||||||
Optional<AuthResult> resultOpt = twitterAuthService.authenticate(
|
Optional<AuthResult> resultOpt = twitterAuthService.authenticate(
|
||||||
@@ -323,7 +328,7 @@ public class AuthController {
|
|||||||
if (resultOpt.isPresent()) {
|
if (resultOpt.isPresent()) {
|
||||||
AuthResult result = resultOpt.get();
|
AuthResult result = resultOpt.get();
|
||||||
if (viaInvite && result.isNewUser()) {
|
if (viaInvite && result.isNewUser()) {
|
||||||
inviteService.consume(req.getInviteToken());
|
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||||
"reason_code", "INVITE_APPROVED"
|
"reason_code", "INVITE_APPROVED"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class CommentController {
|
|||||||
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
||||||
CommentDto dto = commentMapper.toDto(comment);
|
CommentDto dto = commentMapper.toDto(comment);
|
||||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||||
dto.setPointReward(pointService.awardForComment(auth.getName(),postId));
|
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
||||||
log.debug("createComment succeeded for comment {}", comment.getId());
|
log.debug("createComment succeeded for comment {}", comment.getId());
|
||||||
return ResponseEntity.ok(dto);
|
return ResponseEntity.ok(dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.PointHistoryDto;
|
||||||
|
import com.openisle.mapper.PointHistoryMapper;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/point-histories")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PointHistoryController {
|
||||||
|
private final PointService pointService;
|
||||||
|
private final PointHistoryMapper pointHistoryMapper;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PointHistoryDto> list(Authentication auth) {
|
||||||
|
return pointService.listHistory(auth.getName()).stream()
|
||||||
|
.map(pointHistoryMapper::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class PostController {
|
|||||||
draftService.deleteDraft(auth.getName());
|
draftService.deleteDraft(auth.getName());
|
||||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
dto.setPointReward(pointService.awardForPost(auth.getName()));
|
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
||||||
return ResponseEntity.ok(dto);
|
return ResponseEntity.ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.model.Post;
|
import com.openisle.model.Post;
|
||||||
|
import com.openisle.model.Comment;
|
||||||
|
import com.openisle.model.CommentSort;
|
||||||
import com.openisle.service.PostService;
|
import com.openisle.service.PostService;
|
||||||
|
import com.openisle.service.CommentService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
@@ -31,6 +34,7 @@ import java.util.regex.Pattern;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RssController {
|
public class RssController {
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
|
private final CommentService commentService;
|
||||||
|
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
@@ -103,6 +107,12 @@ public class RssController {
|
|||||||
enclosure = absolutifyUrl(enclosure, base);
|
enclosure = absolutifyUrl(enclosure, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 <content:encoded>
|
||||||
|
List<Comment> topComments = commentService
|
||||||
|
.getCommentsForPost(p.getId(), CommentSort.MOST_INTERACTIONS);
|
||||||
|
topComments = topComments.subList(0, Math.min(10, topComments.size()));
|
||||||
|
String footerHtml = buildFooterHtml(base, link, topComments);
|
||||||
|
|
||||||
sb.append("<item>");
|
sb.append("<item>");
|
||||||
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
elem(sb, "title", cdata(nullSafe(p.getTitle())));
|
||||||
elem(sb, "link", link);
|
elem(sb, "link", link);
|
||||||
@@ -110,8 +120,11 @@ 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=\"")
|
||||||
@@ -136,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")
|
||||||
@@ -246,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;
|
||||||
|
}
|
||||||
|
|
||||||
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.PointHistoryType;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class PointHistoryDto {
|
||||||
|
private Long id;
|
||||||
|
private PointHistoryType type;
|
||||||
|
private int amount;
|
||||||
|
private int balance;
|
||||||
|
private Long postId;
|
||||||
|
private String postTitle;
|
||||||
|
private Long commentId;
|
||||||
|
private String commentContent;
|
||||||
|
private Long fromUserId;
|
||||||
|
private String fromUserName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.openisle.mapper;
|
||||||
|
|
||||||
|
import com.openisle.dto.PointHistoryDto;
|
||||||
|
import com.openisle.model.PointHistory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class PointHistoryMapper {
|
||||||
|
public PointHistoryDto toDto(PointHistory history) {
|
||||||
|
PointHistoryDto dto = new PointHistoryDto();
|
||||||
|
dto.setId(history.getId());
|
||||||
|
dto.setType(history.getType());
|
||||||
|
dto.setAmount(history.getAmount());
|
||||||
|
dto.setBalance(history.getBalance());
|
||||||
|
dto.setCreatedAt(history.getCreatedAt());
|
||||||
|
if (history.getPost() != null) {
|
||||||
|
dto.setPostId(history.getPost().getId());
|
||||||
|
dto.setPostTitle(history.getPost().getTitle());
|
||||||
|
}
|
||||||
|
if (history.getComment() != null) {
|
||||||
|
dto.setCommentId(history.getComment().getId());
|
||||||
|
dto.setCommentContent(history.getComment().getContent());
|
||||||
|
if (history.getComment().getPost() != null && dto.getPostId() == null) {
|
||||||
|
dto.setPostId(history.getComment().getPost().getId());
|
||||||
|
dto.setPostTitle(history.getComment().getPost().getTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (history.getFromUser() != null) {
|
||||||
|
dto.setFromUserId(history.getFromUser().getId());
|
||||||
|
dto.setFromUserName(history.getFromUser().getUsername());
|
||||||
|
}
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
49
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
49
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package com.openisle.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** Point change history for a user. */
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table(name = "point_histories")
|
||||||
|
public class PointHistory {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PointHistoryType type;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int amount;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int balance;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "post_id")
|
||||||
|
private Post post;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "comment_id")
|
||||||
|
private Comment comment;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "from_user_id")
|
||||||
|
private User fromUser;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.model;
|
||||||
|
|
||||||
|
public enum PointHistoryType {
|
||||||
|
POST,
|
||||||
|
COMMENT,
|
||||||
|
POST_LIKED,
|
||||||
|
COMMENT_LIKED,
|
||||||
|
INVITE,
|
||||||
|
FEATURE,
|
||||||
|
SYSTEM_ONLINE,
|
||||||
|
REDEEM
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.repository;
|
||||||
|
|
||||||
|
import com.openisle.model.PointHistory;
|
||||||
|
import com.openisle.model.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> {
|
||||||
|
List<PointHistory> findByUserOrderByIdDesc(User user);
|
||||||
|
long countByUser(User user);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.openisle.model.User;
|
|||||||
import com.openisle.repository.InviteTokenRepository;
|
import com.openisle.repository.InviteTokenRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -18,6 +19,12 @@ public class InviteService {
|
|||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class InviteValidateResult {
|
||||||
|
InviteToken inviteToken;
|
||||||
|
boolean validate;
|
||||||
|
}
|
||||||
|
|
||||||
public String generate(String username) {
|
public String generate(String username) {
|
||||||
User inviter = userRepository.findByUsername(username).orElseThrow();
|
User inviter = userRepository.findByUsername(username).orElseThrow();
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
@@ -35,20 +42,23 @@ public class InviteService {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validate(String token) {
|
public InviteValidateResult validate(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return new InviteValidateResult(null, false);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
jwtService.validateAndGetSubjectForInvite(token);
|
jwtService.validateAndGetSubjectForInvite(token);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return new InviteValidateResult(null, false);
|
||||||
}
|
}
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
||||||
return invite != null && invite.getUsageCount() < 3;
|
return new InviteValidateResult(invite, invite != null && invite.getUsageCount() < 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void consume(String token) {
|
public void consume(String token, String newUserName) {
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
||||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||||
inviteTokenRepository.save(invite);
|
inviteTokenRepository.save(invite);
|
||||||
pointService.awardForInvite(invite.getInviter().getUsername());
|
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package com.openisle.service;
|
|||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.exception.NotFoundException;
|
import com.openisle.exception.NotFoundException;
|
||||||
import com.openisle.model.PointGood;
|
import com.openisle.model.PointGood;
|
||||||
|
import com.openisle.model.PointHistory;
|
||||||
|
import com.openisle.model.PointHistoryType;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.PointGoodRepository;
|
import com.openisle.repository.PointGoodRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -18,6 +21,7 @@ public class PointMallService {
|
|||||||
private final PointGoodRepository pointGoodRepository;
|
private final PointGoodRepository pointGoodRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
|
|
||||||
public List<PointGood> listGoods() {
|
public List<PointGood> listGoods() {
|
||||||
return pointGoodRepository.findAll();
|
return pointGoodRepository.findAll();
|
||||||
@@ -32,6 +36,13 @@ public class PointMallService {
|
|||||||
user.setPoint(user.getPoint() - good.getCost());
|
user.setPoint(user.getPoint() - good.getCost());
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
notificationService.createPointRedeemNotifications(user, good.getName() + ": " + contact);
|
notificationService.createPointRedeemNotifications(user, good.getName() + ": " + contact);
|
||||||
|
PointHistory history = new PointHistory();
|
||||||
|
history.setUser(user);
|
||||||
|
history.setType(PointHistoryType.REDEEM);
|
||||||
|
history.setAmount(-good.getCost());
|
||||||
|
history.setBalance(user.getPoint());
|
||||||
|
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||||
|
pointHistoryRepository.save(history);
|
||||||
return user.getPoint();
|
return user.getPoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.openisle.service;
|
package com.openisle.service;
|
||||||
|
|
||||||
import com.openisle.model.PointLog;
|
import com.openisle.model.*;
|
||||||
import com.openisle.model.User;
|
|
||||||
import com.openisle.repository.*;
|
import com.openisle.repository.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -16,19 +15,28 @@ public class PointService {
|
|||||||
private final PointLogRepository pointLogRepository;
|
private final PointLogRepository pointLogRepository;
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
private final CommentRepository commentRepository;
|
private final CommentRepository commentRepository;
|
||||||
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
|
|
||||||
public int awardForPost(String userName) {
|
public int awardForPost(String userName, Long postId) {
|
||||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||||
PointLog log = getTodayLog(user);
|
PointLog log = getTodayLog(user);
|
||||||
if (log.getPostCount() > 1) return 0;
|
if (log.getPostCount() > 1) return 0;
|
||||||
log.setPostCount(log.getPostCount() + 1);
|
log.setPostCount(log.getPostCount() + 1);
|
||||||
pointLogRepository.save(log);
|
pointLogRepository.save(log);
|
||||||
return addPoint(user, 30);
|
Post post = postRepository.findById(postId).orElseThrow();
|
||||||
|
return addPoint(user, 30, PointHistoryType.POST, post, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int awardForInvite(String userName) {
|
public int awardForInvite(String userName, String inviteeName) {
|
||||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||||
return addPoint(user, 500);
|
User invitee = userRepository.findByUsername(inviteeName).orElseThrow();
|
||||||
|
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) {
|
||||||
@@ -45,20 +53,41 @@ public class PointService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private int addPoint(User user, int amount) {
|
private int addPoint(User user, int amount, PointHistoryType type,
|
||||||
|
Post post, Comment comment, User fromUser) {
|
||||||
|
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||||
|
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||||
|
}
|
||||||
user.setPoint(user.getPoint() + amount);
|
user.setPoint(user.getPoint() + amount);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
recordHistory(user, type, amount, post, comment, fromUser);
|
||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordHistory(User user, PointHistoryType type, int amount,
|
||||||
|
Post post, Comment comment, User fromUser) {
|
||||||
|
PointHistory history = new PointHistory();
|
||||||
|
history.setUser(user);
|
||||||
|
history.setType(type);
|
||||||
|
history.setAmount(amount);
|
||||||
|
history.setBalance(user.getPoint());
|
||||||
|
history.setPost(post);
|
||||||
|
history.setComment(comment);
|
||||||
|
history.setFromUser(fromUser);
|
||||||
|
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||||
|
pointHistoryRepository.save(history);
|
||||||
|
}
|
||||||
|
|
||||||
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
|
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
|
||||||
// 注意需要考虑发帖和回复是同一人的场景
|
// 注意需要考虑发帖和回复是同一人的场景
|
||||||
public int awardForComment(String commenterName, Long postId) {
|
public int awardForComment(String commenterName, Long postId, Long commentId) {
|
||||||
// 标记评论者是否已达到积分奖励上限
|
// 标记评论者是否已达到积分奖励上限
|
||||||
boolean isTheRewardCapped = false;
|
boolean isTheRewardCapped = false;
|
||||||
|
|
||||||
// 根据帖子id找到发帖人
|
// 根据帖子id找到发帖人
|
||||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
Post post = postRepository.findById(postId).orElseThrow();
|
||||||
|
User poster = post.getAuthor();
|
||||||
|
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||||
|
|
||||||
// 获取评论者的加分日志
|
// 获取评论者的加分日志
|
||||||
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
|
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
|
||||||
@@ -74,15 +103,15 @@ public class PointService {
|
|||||||
} else {
|
} else {
|
||||||
log.setCommentCount(log.getCommentCount() + 1);
|
log.setCommentCount(log.getCommentCount() + 1);
|
||||||
pointLogRepository.save(log);
|
pointLogRepository.save(log);
|
||||||
return addPoint(commenter, 10);
|
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addPoint(poster, 10);
|
addPoint(poster, 10, PointHistoryType.COMMENT, post, comment, commenter);
|
||||||
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
|
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
|
||||||
if (isTheRewardCapped) {
|
if (isTheRewardCapped) {
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
return addPoint(commenter, 10);
|
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +130,8 @@ public class PointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是同一个,则为发帖人加分
|
// 如果不是同一个,则为发帖人加分
|
||||||
return addPoint(poster, 10);
|
Post post = postRepository.findById(postId).orElseThrow();
|
||||||
|
return addPoint(poster, 10, PointHistoryType.POST_LIKED, post, null, reactioner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 考虑点赞者和评论者是同一个的情况
|
// 考虑点赞者和评论者是同一个的情况
|
||||||
@@ -118,7 +148,17 @@ public class PointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是同一个,则为发帖人加分
|
// 如果不是同一个,则为发帖人加分
|
||||||
return addPoint(commenter, 10);
|
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||||
|
Post post = comment.getPost();
|
||||||
|
return addPoint(commenter, 10, PointHistoryType.COMMENT_LIKED, post, comment, reactioner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<PointHistory> listHistory(String userName) {
|
||||||
|
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||||
|
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||||
|
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||||
|
}
|
||||||
|
return pointHistoryRepository.findByUserOrderByIdDesc(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const checkMilkTeaActivity = async () => {
|
const checkMilkTeaActivity = async () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
if (localStorage.getItem('milkTeaActivityPopupShown')) return
|
if (localStorage.getItem('milkTeaActivityPopupShown')) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
@@ -68,7 +68,7 @@ const checkMilkTeaActivity = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkInviteCodeActivity = async () => {
|
const checkInviteCodeActivity = async () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
if (localStorage.getItem('inviteCodeActivityPopupShown')) return
|
if (localStorage.getItem('inviteCodeActivityPopupShown')) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
@@ -86,30 +86,30 @@ const checkInviteCodeActivity = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeInviteCodePopup = () => {
|
const closeInviteCodePopup = () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
localStorage.setItem('inviteCodeActivityPopupShown', 'true')
|
localStorage.setItem('inviteCodeActivityPopupShown', 'true')
|
||||||
showInviteCodePopup.value = false
|
showInviteCodePopup.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeMilkTeaPopup = () => {
|
const closeMilkTeaPopup = () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
||||||
showMilkTeaPopup.value = false
|
showMilkTeaPopup.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkNotificationSetting = async () => {
|
const checkNotificationSetting = async () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
if (!authState.loggedIn) return
|
if (!authState.loggedIn) return
|
||||||
if (localStorage.getItem('notificationSettingPopupShown')) return
|
if (localStorage.getItem('notificationSettingPopupShown')) return
|
||||||
showNotificationPopup.value = true
|
showNotificationPopup.value = true
|
||||||
}
|
}
|
||||||
const closeNotificationPopup = () => {
|
const closeNotificationPopup = () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
localStorage.setItem('notificationSettingPopupShown', 'true')
|
localStorage.setItem('notificationSettingPopupShown', 'true')
|
||||||
showNotificationPopup.value = false
|
showNotificationPopup.value = false
|
||||||
}
|
}
|
||||||
const checkNewMedals = async () => {
|
const checkNewMedals = async () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
if (!authState.loggedIn || !authState.userId) return
|
if (!authState.loggedIn || !authState.userId) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/medals?userId=${authState.userId}`)
|
const res = await fetch(`${API_BASE_URL}/api/medals?userId=${authState.userId}`)
|
||||||
@@ -127,7 +127,7 @@ const checkNewMedals = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const closeMedalPopup = () => {
|
const closeMedalPopup = () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]'))
|
const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]'))
|
||||||
newMedals.value.forEach((m) => seen.add(m.type))
|
newMedals.value.forEach((m) => seen.add(m.type))
|
||||||
localStorage.setItem('seenMedals', JSON.stringify([...seen]))
|
localStorage.setItem('seenMedals', JSON.stringify([...seen]))
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const stopObserver = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startObserver = () => {
|
const startObserver = () => {
|
||||||
if (!process.client || props.pause || done.value) return
|
if (!import.meta.client || props.pause || done.value) return
|
||||||
stopObserver()
|
stopObserver()
|
||||||
io = new IntersectionObserver(
|
io = new IntersectionObserver(
|
||||||
async (entries) => {
|
async (entries) => {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const isImageIcon = (icon) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildTagsUrl = (kw = '') => {
|
const buildTagsUrl = (kw = '') => {
|
||||||
const base = API_BASE_URL || (process.client ? window.location.origin : '')
|
const base = API_BASE_URL || (import.meta.client ? window.location.origin : '')
|
||||||
const url = new URL('/api/tags', base)
|
const url = new URL('/api/tags', base)
|
||||||
|
|
||||||
if (kw) url.searchParams.set('keyword', kw)
|
if (kw) url.searchParams.set('keyword', kw)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 导出一个便捷的 toast 对象
|
// 导出一个便捷的 toast 对象
|
||||||
export const toast = {
|
export const toast = {
|
||||||
success: async (message) => {
|
success: async (message) => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
try {
|
try {
|
||||||
const { useToast } = await import('vue-toastification')
|
const { useToast } = await import('vue-toastification')
|
||||||
const toastInstance = useToast()
|
const toastInstance = useToast()
|
||||||
@@ -12,7 +12,7 @@ export const toast = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: async (message) => {
|
error: async (message) => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
try {
|
try {
|
||||||
const { useToast } = await import('vue-toastification')
|
const { useToast } = await import('vue-toastification')
|
||||||
const toastInstance = useToast()
|
const toastInstance = useToast()
|
||||||
@@ -23,7 +23,7 @@ export const toast = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
warning: async (message) => {
|
warning: async (message) => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
try {
|
try {
|
||||||
const { useToast } = await import('vue-toastification')
|
const { useToast } = await import('vue-toastification')
|
||||||
const toastInstance = useToast()
|
const toastInstance = useToast()
|
||||||
@@ -34,7 +34,7 @@ export const toast = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
info: async (message) => {
|
info: async (message) => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
try {
|
try {
|
||||||
const { useToast } = await import('vue-toastification')
|
const { useToast } = await import('vue-toastification')
|
||||||
const toastInstance = useToast()
|
const toastInstance = useToast()
|
||||||
@@ -48,7 +48,7 @@ export const toast = {
|
|||||||
|
|
||||||
// 导出 useToast composable
|
// 导出 useToast composable
|
||||||
export const useToast = () => {
|
export const useToast = () => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
try {
|
try {
|
||||||
const { useToast: useVueToast } = await import('vue-toastification')
|
const { useToast: useVueToast } = await import('vue-toastification')
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="site-stats-page">
|
<div class="site-stats-page">
|
||||||
|
<div v-if="isLoading" class="loading-message">
|
||||||
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
|
</div>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<VChart
|
<VChart
|
||||||
v-if="dauOption"
|
v-if="dauOption"
|
||||||
@@ -51,8 +54,10 @@ const dauOption = ref(null)
|
|||||||
const newUserOption = ref(null)
|
const newUserOption = ref(null)
|
||||||
const postOption = ref(null)
|
const postOption = ref(null)
|
||||||
const commentOption = ref(null)
|
const commentOption = ref(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
|
isLoading.value = true
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const headers = { Authorization: `Bearer ${token}` }
|
const headers = { Authorization: `Bearer ${token}` }
|
||||||
|
|
||||||
@@ -93,6 +98,7 @@ async function loadData() {
|
|||||||
const data = await commentRes.json()
|
const data = await commentRes.json()
|
||||||
commentOption.value = toOption('每日回贴量', data)
|
commentOption.value = toOption('每日回贴量', data)
|
||||||
}
|
}
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadData)
|
onMounted(loadData)
|
||||||
@@ -105,4 +111,11 @@ onMounted(loadData)
|
|||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
@@ -338,7 +342,7 @@ watch([selectedCategory, selectedTags], () => {
|
|||||||
watch(selectedTopic, (val) => {
|
watch(selectedTopic, (val) => {
|
||||||
loadOptions()
|
loadOptions()
|
||||||
selectedTopicCookie.value = val
|
selectedTopicCookie.value = val
|
||||||
if (process.client) localStorage.setItem('homeTab', val)
|
if (import.meta.client) localStorage.setItem('homeTab', val)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 选项首屏加载:服务端执行一次;客户端兜底 **/
|
/** 选项首屏加载:服务端执行一次;客户端兜底 **/
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,181 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="point-mall-page">
|
<div class="point-mall-page">
|
||||||
<section class="rules">
|
<div class="point-tabs">
|
||||||
<div class="section-title">🎉 积分规则</div>
|
<div
|
||||||
<div class="section-content">
|
:class="['point-tab-item', { selected: selectedTab === 'mall' }]"
|
||||||
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
@click="selectedTab = 'mall'"
|
||||||
|
>
|
||||||
|
积分兑换
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="['point-tab-item', { selected: selectedTab === 'history' }]"
|
||||||
|
@click="selectedTab = 'history'"
|
||||||
|
>
|
||||||
|
积分历史
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="loading-points-container" v-if="isLoading">
|
|
||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="point-info">
|
<template v-if="selectedTab === 'mall'">
|
||||||
<p v-if="authState.loggedIn && point !== null">
|
<div class="point-mall-page-content">
|
||||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span class="point-value">{{
|
<section class="rules">
|
||||||
point
|
<div class="section-title">🎉 积分规则</div>
|
||||||
}}</span>
|
<div class="section-content">
|
||||||
</p>
|
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="goods">
|
<div class="loading-points-container" v-if="isLoading">
|
||||||
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
<img class="goods-item-image" :src="good.image" alt="good.name" />
|
|
||||||
<div class="goods-item-name">{{ good.name }}</div>
|
|
||||||
<div class="goods-item-cost">
|
|
||||||
<i class="fas fa-coins"></i>
|
|
||||||
{{ good.cost }} 积分
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="goods-item-button"
|
<div class="point-info">
|
||||||
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
<p v-if="authState.loggedIn && point !== null">
|
||||||
@click="openRedeem(good)"
|
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span
|
||||||
>
|
class="point-value"
|
||||||
兑换
|
>{{ point }}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="goods">
|
||||||
|
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
||||||
|
<img class="goods-item-image" :src="good.image" alt="good.name" />
|
||||||
|
<div class="goods-item-name">{{ good.name }}</div>
|
||||||
|
<div class="goods-item-cost">
|
||||||
|
<i class="fas fa-coins"></i>
|
||||||
|
{{ good.cost }} 积分
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="goods-item-button"
|
||||||
|
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
||||||
|
@click="openRedeem(good)"
|
||||||
|
>
|
||||||
|
兑换
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<RedeemPopup
|
||||||
|
:visible="dialogVisible"
|
||||||
|
v-model="contact"
|
||||||
|
:loading="loading"
|
||||||
|
@close="closeRedeem"
|
||||||
|
@submit="submitRedeem"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</template>
|
||||||
<RedeemPopup
|
|
||||||
:visible="dialogVisible"
|
<template v-else>
|
||||||
v-model="contact"
|
<div class="loading-points-container" v-if="historyLoading">
|
||||||
:loading="loading"
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
@close="closeRedeem"
|
</div>
|
||||||
@submit="submitRedeem"
|
<BasePlaceholder v-else-if="histories.length === 0" text="暂无积分记录" icon="fas fa-inbox" />
|
||||||
/>
|
<div class="timeline-container" v-else>
|
||||||
|
<BaseTimeline :items="histories">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div class="history-content">
|
||||||
|
<template v-if="item.type === 'POST'">
|
||||||
|
发送帖子
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'COMMENT'">
|
||||||
|
在文章
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
中
|
||||||
|
<template v-if="!item.fromUserId">
|
||||||
|
发送评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
被评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'POST_LIKED' && item.fromUserId">
|
||||||
|
帖子
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
按赞,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'COMMENT_LIKED' && item.fromUserId">
|
||||||
|
评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
按赞,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'INVITE' && item.fromUserId">
|
||||||
|
邀请了好友
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</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>
|
||||||
|
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
||||||
|
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
||||||
|
</div>
|
||||||
|
<div class="history-time">{{ TimeManager.format(item.createdAt) }}</div>
|
||||||
|
</template>
|
||||||
|
</BaseTimeline>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import { authState, fetchCurrentUser, getToken } from '~/utils/auth'
|
import { authState, fetchCurrentUser, getToken } from '~/utils/auth'
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import RedeemPopup from '~/components/RedeemPopup.vue'
|
import RedeemPopup from '~/components/RedeemPopup.vue'
|
||||||
|
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||||
|
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||||
|
import { stripMarkdownLength } from '~/utils/markdown'
|
||||||
|
import TimeManager from '~/utils/time'
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
|
const selectedTab = ref('mall')
|
||||||
const point = ref(null)
|
const point = ref(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const histories = ref([])
|
||||||
|
const historyLoading = ref(false)
|
||||||
|
const historyLoaded = ref(false)
|
||||||
|
|
||||||
const pointRules = [
|
const pointRules = [
|
||||||
'发帖:每天前两次,每次 30 积分',
|
'发帖:每天前两次,每次 30 积分',
|
||||||
@@ -64,6 +183,7 @@ const pointRules = [
|
|||||||
'帖子被点赞:每次 10 积分',
|
'帖子被点赞:每次 10 积分',
|
||||||
'评论被点赞:每次 10 积分',
|
'评论被点赞:每次 10 积分',
|
||||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
||||||
|
'文章被收录至精选:每次 500 积分',
|
||||||
]
|
]
|
||||||
|
|
||||||
const goods = ref([])
|
const goods = ref([])
|
||||||
@@ -72,6 +192,17 @@ const contact = ref('')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selectedGood = ref(null)
|
const selectedGood = ref(null)
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
POST: 'fas fa-file-alt',
|
||||||
|
COMMENT: 'fas fa-comment',
|
||||||
|
POST_LIKED: 'fas fa-thumbs-up',
|
||||||
|
COMMENT_LIKED: 'fas fa-thumbs-up',
|
||||||
|
INVITE: 'fas fa-user-plus',
|
||||||
|
SYSTEM_ONLINE: 'fas fa-clock',
|
||||||
|
REDEEM: 'fas fa-gift',
|
||||||
|
FEATURE: 'fas fa-star',
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
if (authState.loggedIn) {
|
if (authState.loggedIn) {
|
||||||
@@ -82,6 +213,12 @@ onMounted(async () => {
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(selectedTab, (val) => {
|
||||||
|
if (val === 'history' && !historyLoaded.value) {
|
||||||
|
loadHistory()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const loadGoods = async () => {
|
const loadGoods = async () => {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/point-goods`)
|
const res = await fetch(`${API_BASE_URL}/api/point-goods`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -89,6 +226,26 @@ const loadGoods = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
if (!authState.loggedIn) {
|
||||||
|
historyLoaded.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
historyLoading.value = true
|
||||||
|
const token = getToken()
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/point-histories`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
histories.value = (await res.json()).map((item) => ({
|
||||||
|
...item,
|
||||||
|
icon: iconMap[item.type],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
historyLoading.value = false
|
||||||
|
historyLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const openRedeem = (good) => {
|
const openRedeem = (good) => {
|
||||||
if (!authState.loggedIn || point.value === null || point.value < good.cost) {
|
if (!authState.loggedIn || point.value === null || point.value < good.cost) {
|
||||||
toast.error('积分不足')
|
toast.error('积分不足')
|
||||||
@@ -129,12 +286,44 @@ const submitRedeem = async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.point-mall-page {
|
.point-mall-page {
|
||||||
padding: 0 20px;
|
|
||||||
max-width: var(--page-max-width);
|
max-width: var(--page-max-width);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.point-mall-page-content {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--normal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-tab-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-tab-item.selected {
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-container {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-points-container {
|
.loading-points-container {
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -215,6 +404,17 @@ const submitRedeem = async () => {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-content {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ const commentSort = ref('NEWEST')
|
|||||||
const isFetchingComments = ref(false)
|
const isFetchingComments = ref(false)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const headerHeight = process.client
|
const headerHeight = import.meta.client
|
||||||
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ useHead(() => ({
|
|||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('scroll', updateCurrentIndex)
|
window.removeEventListener('scroll', updateCurrentIndex)
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
if (countdownTimer) clearInterval(countdownTimer)
|
||||||
@@ -355,7 +355,7 @@ const updateCountdown = () => {
|
|||||||
countdown.value = `${h}:${m}:${s}`
|
countdown.value = `${h}:${m}:${s}`
|
||||||
}
|
}
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
if (!process.client) return
|
if (!import.meta.client) return
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
if (countdownTimer) clearInterval(countdownTimer)
|
||||||
updateCountdown()
|
updateCountdown()
|
||||||
countdownTimer = setInterval(updateCountdown, 1000)
|
countdownTimer = setInterval(updateCountdown, 1000)
|
||||||
@@ -515,7 +515,7 @@ watchEffect(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 404 客户端跳转
|
// 404 客户端跳转
|
||||||
// if (postError.value?.statusCode === 404 && process.client) {
|
// if (postError.value?.statusCode === 404 && import.meta.client) {
|
||||||
// router.replace('/404')
|
// router.replace('/404')
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@@ -877,6 +877,7 @@ const gotoProfile = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
|
scrollTo(0, 0)
|
||||||
await fetchComments()
|
await fetchComments()
|
||||||
const hash = location.hash
|
const hash = location.hash
|
||||||
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
|
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { clearToken } from '~/utils/auth'
|
import { clearToken } from '~/utils/auth'
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
const originalFetch = window.fetch
|
const originalFetch = window.fetch
|
||||||
window.fetch = async (input, init) => {
|
window.fetch = async (input, init) => {
|
||||||
const response = await originalFetch(input, init)
|
const response = await originalFetch(input, init)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import '~/assets/toast.css'
|
|||||||
|
|
||||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||||
// 确保只在客户端环境中注册插件
|
// 确保只在客户端环境中注册插件
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
try {
|
try {
|
||||||
// 使用动态导入来避免 CommonJS 模块问题
|
// 使用动态导入来避免 CommonJS 模块问题
|
||||||
const { default: Toast, POSITION } = await import('vue-toastification')
|
const { default: Toast, POSITION } = await import('vue-toastification')
|
||||||
|
|||||||
1
frontend_nuxt/public/tencent2707107139169774686.txt
Normal file
1
frontend_nuxt/public/tencent2707107139169774686.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1839503219847005265
|
||||||
@@ -12,7 +12,7 @@ export const authState = reactive({
|
|||||||
role: null,
|
role: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
authState.loggedIn =
|
authState.loggedIn =
|
||||||
localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== ''
|
localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== ''
|
||||||
authState.userId = localStorage.getItem(USER_ID_KEY)
|
authState.userId = localStorage.getItem(USER_ID_KEY)
|
||||||
@@ -21,18 +21,18 @@ if (process.client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return process.client ? localStorage.getItem(TOKEN_KEY) : null
|
return import.meta.client ? localStorage.getItem(TOKEN_KEY) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setToken(token) {
|
export function setToken(token) {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
localStorage.setItem(TOKEN_KEY, token)
|
localStorage.setItem(TOKEN_KEY, token)
|
||||||
authState.loggedIn = true
|
authState.loggedIn = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearToken() {
|
export function clearToken() {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
localStorage.removeItem(TOKEN_KEY)
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
clearUserInfo()
|
clearUserInfo()
|
||||||
authState.loggedIn = false
|
authState.loggedIn = false
|
||||||
@@ -40,7 +40,7 @@ export function clearToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setUserInfo({ id, username }) {
|
export function setUserInfo({ id, username }) {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
authState.userId = id
|
authState.userId = id
|
||||||
authState.username = username
|
authState.username = username
|
||||||
if (arguments[0] && arguments[0].role) {
|
if (arguments[0] && arguments[0].role) {
|
||||||
@@ -53,7 +53,7 @@ export function setUserInfo({ id, username }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearUserInfo() {
|
export function clearUserInfo() {
|
||||||
if (process.client) {
|
if (import.meta.client) {
|
||||||
localStorage.removeItem(USER_ID_KEY)
|
localStorage.removeItem(USER_ID_KEY)
|
||||||
localStorage.removeItem(USERNAME_KEY)
|
localStorage.removeItem(USERNAME_KEY)
|
||||||
localStorage.removeItem(ROLE_KEY)
|
localStorage.removeItem(ROLE_KEY)
|
||||||
|
|||||||
@@ -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