diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9b9804d2c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +#### **⚠️注意:仅想修改前端的朋友可不用部署后端服务** + +## 如何部署 + +> Step1 先克隆仓库 + +```shell +git clone https://github.com/nagisa77/OpenIsle.git +cd OpenIsle +``` + +> Step2 后端部署 + +```shell +cd backend +``` + +以IDEA编辑器为例,IDEA打开backend文件夹。 + +- 设置VM Option,最好运行在其他端口,非8080,这里设置8081 + +```shell +-Dserver.port=8081 +``` + +![CleanShot 2025-08-04 at 11 .35.49.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/4cf210cfc6ea478a80dfc744c85ccdc4.png) + +- 设置jdk版本为java 17 + +![CleanShot 2025-08-04 at 11 .38.03@2x.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/392eeec753ae436ca12a78f750dfea2d.png) + +- 本机配置MySQL服务(网上很多教程,忽略) +- 设置环境变量.env 文件 或.properties 文件(二选一) + +1. 环境变量文件生成 + +```shell +cp open-isle.env.example open-isle.env +``` + +修改环境变量,留下需要的,比如你要开发Google登录业务,就需要谷歌相关的变量,数据库是一定要的 + +![CleanShot 2025-08-04 at 11 .41.36@2x.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/896c8363b6e64ea19d18c12ec4dae2b4.png) + +应用环境文件, 选择刚刚的`open-isle.env` + +![CleanShot 2025-08-04 at 11 .44.41.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/f588e37838014a6684c141605639b9fa.png) + +2. 直接修改 .properities 文件 + +位置src/main/application.properties, 数据库需要修改标红处,其他按需修改 + +![CleanShot 2025-08-04 at 11 .47.11@2x.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/28c3104448a245419e0b06aee861abb4.png) + +处理完环境问题直接跑起来就能通了 + +![CleanShot 2025-08-04 at 11 .49.01@2x.png](https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/2c945eae44b1477db09e80fc96b5e02d.png) + +> Step3 前端部署 + +前端可以依赖本机部署的后端,也可以直接调用线上的后端接口 + +```shell +cd ../frontend_nuxt/ +``` + +copy环境.env文件 + +```shell +cp .env.staging.example .env +``` + +1. 依赖本机部署的后端:打开本文件夹,修改.env 修改为瞄准本机后端端口 + +```yaml +; 本地部署后端 +NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081 +; 预发环境后端 +; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com +; 生产环境后端 +; NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com +``` + +2. 依赖预发环境后台环境 + +**(⚠️强烈推荐只部署前端的朋友使用该环境)** + +```yaml +; 本地部署后端 +; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081 +; 预发环境后端 +NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com +; 生产环境后端 +; NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com +``` + +4. 依赖线上后台环境 + +```yaml +; 本地部署后端 +; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081 +; 预发环境后端 +; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com +; 生产环境后端 +NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com +``` + +```shell +# 安装依赖 +npm install --verbose + +# 运行前端服务 +npm run dev +``` + +如此一来,浏览器访问 http://127.0.0.1:3000 即可访问前端页面 diff --git a/README.md b/README.md index e1d721d01..a394d7574 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,18 @@

OpenIsle -

- 高效的开源社区前后端端平台 -

- +
+ 高效的开源社区前后端平台 +


+ Image

## 💡 简介 OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台,提供用户注册、登录、贴文发布、评论交互等完整功能,可用于项目社区或直接打造自主社区站点。 -## 🚧 开发 +## 🚧 开发 & 部署 -### 后端 - -1. 确保安装 JDK 17 及 Maven -2. 信息配置修改 `src/main/resources/application.properties`,或通过环境变量设置数据库等参数 -3. 执行 `mvn clean package` 生成包,之后使用 `java -jar target/openisle-0.0.1-SNAPSHOT.jar`启动,或在开发时直接使用 `mvn spring-boot:run` - -### 前端 - -1. 进入前端目录 - ```bash - cd frontend_nuxt - ``` -2. 安装依赖 - ```bash - npm install - ``` -3. 启动开发服务 - ```bash - npm run dev - ``` - - 生产版本使用如下命令编译: - - ```bash - npm run build - ``` - - 会在 `.output` 目录生成文件,配合线上网站方式部署 +详细见 [Contributing](https://github.com/nagisa77/OpenIsle?tab=contributing-ov-file) ## ✨ 项目特点 diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index bad3abcfe..d4a7a07da 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -47,13 +47,14 @@ public class AuthController { return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); } 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", "邀请码使用次数过多")); } try { User user = userService.registerWithInvite( req.getUsername(), req.getEmail(), req.getPassword()); - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), user.getUsername()); emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(user.getUsername()), @@ -144,7 +145,8 @@ public class AuthController { @PostMapping("/google") public ResponseEntity loginWithGoogle(@RequestBody GoogleLoginRequest req) { 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")); } Optional resultOpt = googleAuthService.authenticate( @@ -154,7 +156,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -218,7 +220,8 @@ public class AuthController { @PostMapping("/github") public ResponseEntity loginWithGithub(@RequestBody GithubLoginRequest req) { 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")); } Optional resultOpt = githubAuthService.authenticate( @@ -229,7 +232,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -265,7 +268,8 @@ public class AuthController { @PostMapping("/discord") public ResponseEntity loginWithDiscord(@RequestBody DiscordLoginRequest req) { 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")); } Optional resultOpt = discordAuthService.authenticate( @@ -276,7 +280,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -311,7 +315,8 @@ public class AuthController { @PostMapping("/twitter") public ResponseEntity loginWithTwitter(@RequestBody TwitterLoginRequest req) { 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")); } Optional resultOpt = twitterAuthService.authenticate( @@ -323,7 +328,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" diff --git a/backend/src/main/java/com/openisle/controller/CommentController.java b/backend/src/main/java/com/openisle/controller/CommentController.java index 09e998607..77b3b0b16 100644 --- a/backend/src/main/java/com/openisle/controller/CommentController.java +++ b/backend/src/main/java/com/openisle/controller/CommentController.java @@ -47,7 +47,7 @@ public class CommentController { Comment comment = commentService.addComment(auth.getName(), postId, req.getContent()); CommentDto dto = commentMapper.toDto(comment); 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()); return ResponseEntity.ok(dto); } diff --git a/backend/src/main/java/com/openisle/controller/PointHistoryController.java b/backend/src/main/java/com/openisle/controller/PointHistoryController.java new file mode 100644 index 000000000..a547d309a --- /dev/null +++ b/backend/src/main/java/com/openisle/controller/PointHistoryController.java @@ -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 list(Authentication auth) { + return pointService.listHistory(auth.getName()).stream() + .map(pointHistoryMapper::toDto) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 3dd4662e0..3ea7334e2 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -45,7 +45,7 @@ public class PostController { draftService.deleteDraft(auth.getName()); PostDetailDto dto = postMapper.toDetailDto(post, 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); } @@ -171,4 +171,27 @@ public class PostController { return postService.listPostsByLatestReply(ids, tids, page, pageSize) .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); } + + @GetMapping("/featured") + public List featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, + @RequestParam(value = "categoryIds", required = false) List categoryIds, + @RequestParam(value = "tagId", required = false) Long tagId, + @RequestParam(value = "tagIds", required = false) List tagIds, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "pageSize", required = false) Integer pageSize, + Authentication auth) { + List ids = categoryIds; + if (categoryId != null) { + ids = java.util.List.of(categoryId); + } + List tids = tagIds; + if (tagId != null) { + tids = java.util.List.of(tagId); + } + if (auth != null) { + userVisitService.recordVisit(auth.getName()); + } + return postService.listFeaturedPosts(ids, tids, page, pageSize) + .stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); + } } diff --git a/backend/src/main/java/com/openisle/controller/RssController.java b/backend/src/main/java/com/openisle/controller/RssController.java index 4970ebcc1..7dc6122f7 100644 --- a/backend/src/main/java/com/openisle/controller/RssController.java +++ b/backend/src/main/java/com/openisle/controller/RssController.java @@ -1,7 +1,10 @@ package com.openisle.controller; import com.openisle.model.Post; +import com.openisle.model.Comment; +import com.openisle.model.CommentSort; import com.openisle.service.PostService; +import com.openisle.service.CommentService; import lombok.RequiredArgsConstructor; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -31,6 +34,7 @@ import java.util.regex.Pattern; @RequiredArgsConstructor public class RssController { private final PostService postService; + private final CommentService commentService; @Value("${app.website-url:https://www.open-isle.com}") private String websiteUrl; @@ -103,6 +107,12 @@ public class RssController { enclosure = absolutifyUrl(enclosure, base); } + // 6) 构造优雅的附加区块(原文链接 + 精选评论),编入 + List 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(""); elem(sb, "title", cdata(nullSafe(p.getTitle()))); elem(sb, "link", link); @@ -110,8 +120,11 @@ public class RssController { elem(sb, "pubDate", toRfc1123Gmt(p.getCreatedAt().atZone(ZoneId.systemDefault()))); // 摘要 elem(sb, "description", cdata(plain)); - // 全文(HTML) - sb.append(""); + // 全文(HTML):正文 + 优雅的 Markdown 区块(已转 HTML) + sb.append(""); // 首图 enclosure(图片类型) if (enclosure != null) { sb.append(" 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 ===================== */ private static String toRfc1123Gmt(ZonedDateTime zdt) { diff --git a/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java b/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java new file mode 100644 index 000000000..2e5cbaf9e --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/FeaturedMedalDto.java @@ -0,0 +1,12 @@ +package com.openisle.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class FeaturedMedalDto extends MedalDto { + private long currentFeaturedCount; + private long targetFeaturedCount; +} + diff --git a/backend/src/main/java/com/openisle/dto/PointHistoryDto.java b/backend/src/main/java/com/openisle/dto/PointHistoryDto.java new file mode 100644 index 000000000..cae0b6f6b --- /dev/null +++ b/backend/src/main/java/com/openisle/dto/PointHistoryDto.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/mapper/PointHistoryMapper.java b/backend/src/main/java/com/openisle/mapper/PointHistoryMapper.java new file mode 100644 index 000000000..9a3881d5a --- /dev/null +++ b/backend/src/main/java/com/openisle/mapper/PointHistoryMapper.java @@ -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; + } +} diff --git a/backend/src/main/java/com/openisle/model/MedalType.java b/backend/src/main/java/com/openisle/model/MedalType.java index 00553511d..c6509cebb 100644 --- a/backend/src/main/java/com/openisle/model/MedalType.java +++ b/backend/src/main/java/com/openisle/model/MedalType.java @@ -3,6 +3,7 @@ package com.openisle.model; public enum MedalType { COMMENT, POST, + FEATURED, CONTRIBUTOR, SEED, PIONEER diff --git a/backend/src/main/java/com/openisle/model/NotificationType.java b/backend/src/main/java/com/openisle/model/NotificationType.java index 132af5176..c4b4e0e25 100644 --- a/backend/src/main/java/com/openisle/model/NotificationType.java +++ b/backend/src/main/java/com/openisle/model/NotificationType.java @@ -14,6 +14,8 @@ public enum NotificationType { POST_REVIEW_REQUEST, /** Your post under review was approved or rejected */ POST_REVIEWED, + /** An administrator deleted your post */ + POST_DELETED, /** A subscribed post received a new comment */ POST_UPDATED, /** Someone subscribed to your post */ @@ -38,6 +40,8 @@ public enum NotificationType { LOTTERY_WIN, /** Your lottery post was drawn */ LOTTERY_DRAW, + /** Your post was featured */ + POST_FEATURED, /** You were mentioned in a post or comment */ MENTION } diff --git a/backend/src/main/java/com/openisle/model/PointHistory.java b/backend/src/main/java/com/openisle/model/PointHistory.java new file mode 100644 index 000000000..347d4c75a --- /dev/null +++ b/backend/src/main/java/com/openisle/model/PointHistory.java @@ -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; +} diff --git a/backend/src/main/java/com/openisle/model/PointHistoryType.java b/backend/src/main/java/com/openisle/model/PointHistoryType.java new file mode 100644 index 000000000..af03d989c --- /dev/null +++ b/backend/src/main/java/com/openisle/model/PointHistoryType.java @@ -0,0 +1,12 @@ +package com.openisle.model; + +public enum PointHistoryType { + POST, + COMMENT, + POST_LIKED, + COMMENT_LIKED, + INVITE, + FEATURE, + SYSTEM_ONLINE, + REDEEM +} diff --git a/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java new file mode 100644 index 000000000..ac1ee7096 --- /dev/null +++ b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java @@ -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 { + List findByUserOrderByIdDesc(User user); + long countByUser(User user); +} diff --git a/backend/src/main/java/com/openisle/repository/PostRepository.java b/backend/src/main/java/com/openisle/repository/PostRepository.java index 58083b193..a072c83f1 100644 --- a/backend/src/main/java/com/openisle/repository/PostRepository.java +++ b/backend/src/main/java/com/openisle/repository/PostRepository.java @@ -97,6 +97,8 @@ public interface PostRepository extends JpaRepository { long countDistinctByTags_Id(Long tagId); + long countByAuthor_IdAndRssExcludedFalse(Long userId); + @Query("SELECT t.id, COUNT(DISTINCT p) FROM Post p JOIN p.tags t WHERE t.id IN :tagIds GROUP BY t.id") List countPostsByTagIds(@Param("tagIds") List tagIds); diff --git a/backend/src/main/java/com/openisle/service/InviteService.java b/backend/src/main/java/com/openisle/service/InviteService.java index cd0f895a3..23ca58bd1 100644 --- a/backend/src/main/java/com/openisle/service/InviteService.java +++ b/backend/src/main/java/com/openisle/service/InviteService.java @@ -5,6 +5,7 @@ import com.openisle.model.User; import com.openisle.repository.InviteTokenRepository; import com.openisle.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.Value; import org.springframework.stereotype.Service; import java.time.LocalDate; @@ -18,6 +19,12 @@ public class InviteService { private final JwtService jwtService; private final PointService pointService; + @Value + public class InviteValidateResult { + InviteToken inviteToken; + boolean validate; + } + public String generate(String username) { User inviter = userRepository.findByUsername(username).orElseThrow(); LocalDate today = LocalDate.now(); @@ -35,20 +42,23 @@ public class InviteService { return token; } - public boolean validate(String token) { + public InviteValidateResult validate(String token) { + if (token == null || token.isEmpty()) { + return new InviteValidateResult(null, false); + } try { jwtService.validateAndGetSubjectForInvite(token); } catch (Exception e) { - return false; + return new InviteValidateResult(null, false); } 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(); invite.setUsageCount(invite.getUsageCount() + 1); inviteTokenRepository.save(invite); - pointService.awardForInvite(invite.getInviter().getUsername()); + pointService.awardForInvite(invite.getInviter().getUsername(), newUserName); } } diff --git a/backend/src/main/java/com/openisle/service/MedalService.java b/backend/src/main/java/com/openisle/service/MedalService.java index daa4acc31..1f43caccd 100644 --- a/backend/src/main/java/com/openisle/service/MedalService.java +++ b/backend/src/main/java/com/openisle/service/MedalService.java @@ -6,6 +6,7 @@ import com.openisle.dto.MedalDto; import com.openisle.dto.PostMedalDto; import com.openisle.dto.SeedUserMedalDto; import com.openisle.dto.PioneerMedalDto; +import com.openisle.dto.FeaturedMedalDto; import com.openisle.model.MedalType; import com.openisle.model.User; import com.openisle.repository.CommentRepository; @@ -74,6 +75,23 @@ public class MedalService { postMedal.setSelected(selected == MedalType.POST); medals.add(postMedal); + FeaturedMedalDto featuredMedal = new FeaturedMedalDto(); + featuredMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_rss.png"); + featuredMedal.setTitle("精选作者"); + featuredMedal.setDescription("至少有1篇文章被收录为精选"); + featuredMedal.setType(MedalType.FEATURED); + featuredMedal.setTargetFeaturedCount(1); + if (user != null) { + long count = postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()); + featuredMedal.setCurrentFeaturedCount(count); + featuredMedal.setCompleted(count >= 1); + } else { + featuredMedal.setCurrentFeaturedCount(0); + featuredMedal.setCompleted(false); + } + featuredMedal.setSelected(selected == MedalType.FEATURED); + medals.add(featuredMedal); + ContributorMedalDto contributorMedal = new ContributorMedalDto(); contributorMedal.setIcon("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/achi_coder.png"); contributorMedal.setTitle("贡献者"); @@ -141,6 +159,8 @@ public class MedalService { user.setDisplayMedal(MedalType.COMMENT); } else if (postRepository.countByAuthor_Id(user.getId()) >= POST_TARGET) { user.setDisplayMedal(MedalType.POST); + } else if (postRepository.countByAuthor_IdAndRssExcludedFalse(user.getId()) >= 1) { + user.setDisplayMedal(MedalType.FEATURED); } else if (contributorService.getContributionLines(user.getUsername()) >= CONTRIBUTION_TARGET) { user.setDisplayMedal(MedalType.CONTRIBUTOR); } else if (userRepository.countByCreatedAtBefore(user.getCreatedAt()) < PIONEER_LIMIT) { diff --git a/backend/src/main/java/com/openisle/service/PointMallService.java b/backend/src/main/java/com/openisle/service/PointMallService.java index c930ca7f6..0f3965b52 100644 --- a/backend/src/main/java/com/openisle/service/PointMallService.java +++ b/backend/src/main/java/com/openisle/service/PointMallService.java @@ -3,8 +3,11 @@ package com.openisle.service; import com.openisle.exception.FieldException; import com.openisle.exception.NotFoundException; import com.openisle.model.PointGood; +import com.openisle.model.PointHistory; +import com.openisle.model.PointHistoryType; import com.openisle.model.User; import com.openisle.repository.PointGoodRepository; +import com.openisle.repository.PointHistoryRepository; import com.openisle.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -18,6 +21,7 @@ public class PointMallService { private final PointGoodRepository pointGoodRepository; private final UserRepository userRepository; private final NotificationService notificationService; + private final PointHistoryRepository pointHistoryRepository; public List listGoods() { return pointGoodRepository.findAll(); @@ -32,6 +36,13 @@ public class PointMallService { user.setPoint(user.getPoint() - good.getCost()); userRepository.save(user); 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(); } } diff --git a/backend/src/main/java/com/openisle/service/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index be46b1fc6..2b5a53060 100644 --- a/backend/src/main/java/com/openisle/service/PointService.java +++ b/backend/src/main/java/com/openisle/service/PointService.java @@ -1,7 +1,6 @@ package com.openisle.service; -import com.openisle.model.PointLog; -import com.openisle.model.User; +import com.openisle.model.*; import com.openisle.repository.*; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -16,19 +15,28 @@ public class PointService { private final PointLogRepository pointLogRepository; private final PostRepository postRepository; 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(); PointLog log = getTodayLog(user); if (log.getPostCount() > 1) return 0; log.setPostCount(log.getPostCount() + 1); 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(); - 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) { @@ -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); userRepository.save(user); + recordHistory(user, type, amount, post, comment, fromUser); 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; // 根据帖子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(); @@ -74,15 +103,15 @@ public class PointService { } else { log.setCommentCount(log.getCommentCount() + 1); pointLogRepository.save(log); - return addPoint(commenter, 10); + return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null); } } else { - addPoint(poster, 10); + addPoint(poster, 10, PointHistoryType.COMMENT, post, comment, commenter); // 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况 if (isTheRewardCapped) { return 0; } 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 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); } } diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index e9a205fb9..a3038d478 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -67,6 +67,7 @@ public class PostService { private final TaskScheduler taskScheduler; private final EmailSender emailSender; private final ApplicationContext applicationContext; + private final PointService pointService; private final ConcurrentMap> scheduledFinalizations = new ConcurrentHashMap<>(); @Value("${app.website-url:https://www.open-isle.com}") private String websiteUrl; @@ -89,6 +90,7 @@ public class PostService { TaskScheduler taskScheduler, EmailSender emailSender, ApplicationContext applicationContext, + PointService pointService, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) { this.postRepository = postRepository; this.userRepository = userRepository; @@ -107,6 +109,7 @@ public class PostService { this.taskScheduler = taskScheduler; this.emailSender = emailSender; this.applicationContext = applicationContext; + this.pointService = pointService; this.publishMode = publishMode; } @@ -146,7 +149,10 @@ public class PostService { public Post includeInRss(Long id) { Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); post.setRssExcluded(false); - return postRepository.save(post); + post = postRepository.save(post); + notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null); + pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId()); + return post; } public Post createPost(String username, @@ -458,6 +464,34 @@ public class PostService { return paginate(sortByPinnedAndCreated(posts), page, pageSize); } + public List listFeaturedPosts(List categoryIds, + List tagIds, + Integer page, + Integer pageSize) { + List posts; + boolean hasCategories = categoryIds != null && !categoryIds.isEmpty(); + boolean hasTags = tagIds != null && !tagIds.isEmpty(); + + if (hasCategories && hasTags) { + posts = listPostsByCategoriesAndTags(categoryIds, tagIds, null, null); + } else if (hasCategories) { + posts = listPostsByCategories(categoryIds, null, null); + } else if (hasTags) { + posts = listPostsByTags(tagIds, null, null); + } else { + posts = listPosts(); + } + + // 仅保留 getRssExcluded 为 0 且不为空 + // 若字段类型是 Boolean(包装类型),0 等价于 false: + posts = posts.stream() + .filter(p -> p.getRssExcluded() != null && !p.getRssExcluded()) + .toList(); + + return paginate(sortByPinnedAndCreated(posts), page, pageSize); + } + + public List listPendingPosts() { return postRepository.findByStatus(PostStatus.PENDING); } @@ -579,7 +613,9 @@ public class PostService { .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); User user = userRepository.findByUsername(username) .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); - if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) { + User author = post.getAuthor(); + boolean adminDeleting = !user.getId().equals(author.getId()) && user.getRole() == Role.ADMIN; + if (!user.getId().equals(author.getId()) && user.getRole() != Role.ADMIN) { throw new IllegalArgumentException("Unauthorized"); } for (Comment c : commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post)) { @@ -596,7 +632,12 @@ public class PostService { future.cancel(false); } } + String title = post.getTitle(); postRepository.delete(post); + if (adminDeleting) { + notificationService.createNotification(author, NotificationType.POST_DELETED, + null, null, null, user, null, title); + } } public java.util.List getPostsByIds(java.util.List ids) { diff --git a/backend/src/test/java/com/openisle/service/MedalServiceTest.java b/backend/src/test/java/com/openisle/service/MedalServiceTest.java index a4a10a56d..a873ea9c6 100644 --- a/backend/src/test/java/com/openisle/service/MedalServiceTest.java +++ b/backend/src/test/java/com/openisle/service/MedalServiceTest.java @@ -27,7 +27,7 @@ class MedalServiceTest { List medals = service.getMedals(null); medals.forEach(m -> assertFalse(m.isCompleted())); - assertEquals(5, medals.size()); + assertEquals(6, medals.size()); } @Test diff --git a/backend/src/test/java/com/openisle/service/PostServiceTest.java b/backend/src/test/java/com/openisle/service/PostServiceTest.java index e1dbfd297..6abe97238 100644 --- a/backend/src/test/java/com/openisle/service/PostServiceTest.java +++ b/backend/src/test/java/com/openisle/service/PostServiceTest.java @@ -34,11 +34,12 @@ class PostServiceTest { TaskScheduler taskScheduler = mock(TaskScheduler.class); EmailSender emailSender = mock(EmailSender.class); ApplicationContext context = mock(ApplicationContext.class); + PointService pointService = mock(PointService.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, notifService, subService, commentService, commentRepo, reactionRepo, subRepo, notificationRepo, postReadService, - imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT); + imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); when(context.getBean(PostService.class)).thenReturn(service); Post post = new Post(); @@ -61,6 +62,59 @@ class PostServiceTest { verify(postRepo).delete(post); } + @Test + void deletePostByAdminNotifiesAuthor() { + PostRepository postRepo = mock(PostRepository.class); + UserRepository userRepo = mock(UserRepository.class); + CategoryRepository catRepo = mock(CategoryRepository.class); + TagRepository tagRepo = mock(TagRepository.class); + LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class); + NotificationService notifService = mock(NotificationService.class); + SubscriptionService subService = mock(SubscriptionService.class); + CommentService commentService = mock(CommentService.class); + CommentRepository commentRepo = mock(CommentRepository.class); + ReactionRepository reactionRepo = mock(ReactionRepository.class); + PostSubscriptionRepository subRepo = mock(PostSubscriptionRepository.class); + NotificationRepository notificationRepo = mock(NotificationRepository.class); + PostReadService postReadService = mock(PostReadService.class); + ImageUploader imageUploader = mock(ImageUploader.class); + TaskScheduler taskScheduler = mock(TaskScheduler.class); + EmailSender emailSender = mock(EmailSender.class); + ApplicationContext context = mock(ApplicationContext.class); + PointService pointService = mock(PointService.class); + + PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, + notifService, subService, commentService, commentRepo, + reactionRepo, subRepo, notificationRepo, postReadService, + imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); + when(context.getBean(PostService.class)).thenReturn(service); + + Post post = new Post(); + post.setId(1L); + post.setTitle("T"); + post.setContent(""); + User author = new User(); + author.setId(2L); + author.setRole(Role.USER); + post.setAuthor(author); + + User admin = new User(); + admin.setId(1L); + admin.setRole(Role.ADMIN); + + when(postRepo.findById(1L)).thenReturn(Optional.of(post)); + when(userRepo.findByUsername("admin")).thenReturn(Optional.of(admin)); + when(commentRepo.findByPostAndParentIsNullOrderByCreatedAtAsc(post)).thenReturn(List.of()); + when(reactionRepo.findByPost(post)).thenReturn(List.of()); + when(subRepo.findByPost(post)).thenReturn(List.of()); + when(notificationRepo.findByPost(post)).thenReturn(List.of()); + + service.deletePost(1L, "admin"); + + verify(notifService).createNotification(eq(author), eq(NotificationType.POST_DELETED), isNull(), + isNull(), isNull(), eq(admin), isNull(), eq("T")); + } + @Test void createPostRespectsRateLimit() { PostRepository postRepo = mock(PostRepository.class); @@ -80,11 +134,12 @@ class PostServiceTest { TaskScheduler taskScheduler = mock(TaskScheduler.class); EmailSender emailSender = mock(EmailSender.class); ApplicationContext context = mock(ApplicationContext.class); + PointService pointService = mock(PointService.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, notifService, subService, commentService, commentRepo, 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(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); @@ -113,11 +168,12 @@ class PostServiceTest { TaskScheduler taskScheduler = mock(TaskScheduler.class); EmailSender emailSender = mock(EmailSender.class); ApplicationContext context = mock(ApplicationContext.class); + PointService pointService = mock(PointService.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, notifService, subService, commentService, commentRepo, reactionRepo, subRepo, notificationRepo, postReadService, - imageUploader, taskScheduler, emailSender, context, PublishMode.DIRECT); + imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); when(context.getBean(PostService.class)).thenReturn(service); User author = new User(); diff --git a/frontend_nuxt/assets/global.css b/frontend_nuxt/assets/global.css index 6b29dcabd..edfad14a8 100644 --- a/frontend_nuxt/assets/global.css +++ b/frontend_nuxt/assets/global.css @@ -184,7 +184,7 @@ body { font-family: 'Maple Mono', monospace; font-size: 13px; border-radius: 4px; - white-space: no-wrap; + white-space: break-spaces; background-color: var(--code-highlight-background-color); color: var(--text-color); } diff --git a/frontend_nuxt/components/AchievementList.vue b/frontend_nuxt/components/AchievementList.vue index a58e4b4a2..ddcceef94 100644 --- a/frontend_nuxt/components/AchievementList.vue +++ b/frontend_nuxt/components/AchievementList.vue @@ -26,6 +26,9 @@ + diff --git a/frontend_nuxt/components/BaseSwitch.vue b/frontend_nuxt/components/BaseSwitch.vue new file mode 100644 index 000000000..f04197c92 --- /dev/null +++ b/frontend_nuxt/components/BaseSwitch.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend_nuxt/components/GlobalPopups.vue b/frontend_nuxt/components/GlobalPopups.vue index f5d87b2eb..90a0f00c7 100644 --- a/frontend_nuxt/components/GlobalPopups.vue +++ b/frontend_nuxt/components/GlobalPopups.vue @@ -50,7 +50,7 @@ onMounted(async () => { }) const checkMilkTeaActivity = async () => { - if (!process.client) return + if (!import.meta.client) return if (localStorage.getItem('milkTeaActivityPopupShown')) return try { const res = await fetch(`${API_BASE_URL}/api/activities`) @@ -68,7 +68,7 @@ const checkMilkTeaActivity = async () => { } const checkInviteCodeActivity = async () => { - if (!process.client) return + if (!import.meta.client) return if (localStorage.getItem('inviteCodeActivityPopupShown')) return try { const res = await fetch(`${API_BASE_URL}/api/activities`) @@ -86,32 +86,30 @@ const checkInviteCodeActivity = async () => { } const closeInviteCodePopup = () => { - if (!process.client) return + if (!import.meta.client) return localStorage.setItem('inviteCodeActivityPopupShown', 'true') showInviteCodePopup.value = false } const closeMilkTeaPopup = () => { - if (!process.client) return + if (!import.meta.client) return localStorage.setItem('milkTeaActivityPopupShown', 'true') showMilkTeaPopup.value = false - checkNotificationSetting() } const checkNotificationSetting = async () => { - if (!process.client) return + if (!import.meta.client) return if (!authState.loggedIn) return if (localStorage.getItem('notificationSettingPopupShown')) return showNotificationPopup.value = true } const closeNotificationPopup = () => { - if (!process.client) return + if (!import.meta.client) return localStorage.setItem('notificationSettingPopupShown', 'true') showNotificationPopup.value = false - checkNewMedals() } const checkNewMedals = async () => { - if (!process.client) return + if (!import.meta.client) return if (!authState.loggedIn || !authState.userId) return try { const res = await fetch(`${API_BASE_URL}/api/medals?userId=${authState.userId}`) @@ -129,7 +127,7 @@ const checkNewMedals = async () => { } } const closeMedalPopup = () => { - if (!process.client) return + if (!import.meta.client) return const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]')) newMedals.value.forEach((m) => seen.add(m.type)) localStorage.setItem('seenMedals', JSON.stringify([...seen])) diff --git a/frontend_nuxt/components/InfiniteLoadMore.vue b/frontend_nuxt/components/InfiniteLoadMore.vue index 441c0ecf4..c4e1a9e0a 100644 --- a/frontend_nuxt/components/InfiniteLoadMore.vue +++ b/frontend_nuxt/components/InfiniteLoadMore.vue @@ -40,7 +40,7 @@ const stopObserver = () => { } const startObserver = () => { - if (!process.client || props.pause || done.value) return + if (!import.meta.client || props.pause || done.value) return stopObserver() io = new IntersectionObserver( async (entries) => { diff --git a/frontend_nuxt/components/TagSelect.vue b/frontend_nuxt/components/TagSelect.vue index b80f506a2..14de024a7 100644 --- a/frontend_nuxt/components/TagSelect.vue +++ b/frontend_nuxt/components/TagSelect.vue @@ -63,7 +63,7 @@ const isImageIcon = (icon) => { } 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) if (kw) url.searchParams.set('keyword', kw) diff --git a/frontend_nuxt/composables/useToast.js b/frontend_nuxt/composables/useToast.js index 900aa1f74..e8daff063 100644 --- a/frontend_nuxt/composables/useToast.js +++ b/frontend_nuxt/composables/useToast.js @@ -1,7 +1,7 @@ // 导出一个便捷的 toast 对象 export const toast = { success: async (message) => { - if (process.client) { + if (import.meta.client) { try { const { useToast } = await import('vue-toastification') const toastInstance = useToast() @@ -12,7 +12,7 @@ export const toast = { } }, error: async (message) => { - if (process.client) { + if (import.meta.client) { try { const { useToast } = await import('vue-toastification') const toastInstance = useToast() @@ -23,7 +23,7 @@ export const toast = { } }, warning: async (message) => { - if (process.client) { + if (import.meta.client) { try { const { useToast } = await import('vue-toastification') const toastInstance = useToast() @@ -34,7 +34,7 @@ export const toast = { } }, info: async (message) => { - if (process.client) { + if (import.meta.client) { try { const { useToast } = await import('vue-toastification') const toastInstance = useToast() @@ -48,7 +48,7 @@ export const toast = { // 导出 useToast composable export const useToast = () => { - if (process.client) { + if (import.meta.client) { return new Promise(async (resolve) => { try { const { useToast: useVueToast } = await import('vue-toastification') diff --git a/frontend_nuxt/pages/about/stats.vue b/frontend_nuxt/pages/about/stats.vue index 74d02898b..7b881d67d 100644 --- a/frontend_nuxt/pages/about/stats.vue +++ b/frontend_nuxt/pages/about/stats.vue @@ -1,5 +1,8 @@