diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index bad3abcfe..f643e7807 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -53,7 +53,7 @@ public class AuthController { 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()), @@ -154,7 +154,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), user.getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -229,7 +229,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), user.getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -276,7 +276,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), user.getUsername()); return ResponseEntity.ok(Map.of( "token", jwtService.generateToken(result.getUser().getUsername()), "reason_code", "INVITE_APPROVED" @@ -323,7 +323,7 @@ public class AuthController { if (resultOpt.isPresent()) { AuthResult result = resultOpt.get(); if (viaInvite && result.isNewUser()) { - inviteService.consume(req.getInviteToken()); + inviteService.consume(req.getInviteToken(), user.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..86d8fd9d1 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); } 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/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..ceda76185 --- /dev/null +++ b/backend/src/main/java/com/openisle/model/PointHistoryType.java @@ -0,0 +1,10 @@ +package com.openisle.model; + +public enum PointHistoryType { + POST, + COMMENT, + POST_LIKED, + COMMENT_LIKED, + INVITE, + SYSTEM_ONLINE +} 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/service/InviteService.java b/backend/src/main/java/com/openisle/service/InviteService.java index cd0f895a3..158772d34 100644 --- a/backend/src/main/java/com/openisle/service/InviteService.java +++ b/backend/src/main/java/com/openisle/service/InviteService.java @@ -45,10 +45,10 @@ public class InviteService { return 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/PointService.java b/backend/src/main/java/com/openisle/service/PointService.java index be46b1fc6..ea6507d98 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,22 @@ 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); } private PointLog getTodayLog(User user) { @@ -45,20 +47,38 @@ 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) { 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 +94,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 +121,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 +139,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/frontend_nuxt/pages/points.vue b/frontend_nuxt/pages/points.vue index 31ae9a7d1..6f8e79fba 100644 --- a/frontend_nuxt/pages/points.vue +++ b/frontend_nuxt/pages/points.vue @@ -1,62 +1,151 @@