Merge pull request #423 from WilliamColton/main

增加积分系统
This commit is contained in:
Tim
2025-08-07 20:04:03 +08:00
committed by GitHub
13 changed files with 308 additions and 91 deletions

View File

@@ -7,6 +7,7 @@ import com.openisle.mapper.CommentMapper;
import com.openisle.service.CaptchaService;
import com.openisle.service.CommentService;
import com.openisle.service.LevelService;
import com.openisle.service.PointService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -26,6 +27,7 @@ public class CommentController {
private final LevelService levelService;
private final CaptchaService captchaService;
private final CommentMapper commentMapper;
private final PointService pointService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@@ -45,6 +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));
log.debug("createComment succeeded for comment {}", comment.getId());
return ResponseEntity.ok(dto);
}

View File

@@ -5,12 +5,7 @@ import com.openisle.dto.PostRequest;
import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper;
import com.openisle.model.Post;
import com.openisle.service.CaptchaService;
import com.openisle.service.DraftService;
import com.openisle.service.LevelService;
import com.openisle.service.PostService;
import com.openisle.service.SubscriptionService;
import com.openisle.service.UserVisitService;
import com.openisle.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@@ -25,12 +20,12 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
private final SubscriptionService subscriptionService;
private final LevelService levelService;
private final CaptchaService captchaService;
private final DraftService draftService;
private final UserVisitService userVisitService;
private final PostMapper postMapper;
private final PointService pointService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@@ -48,6 +43,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()));
return ResponseEntity.ok(dto);
}

View File

@@ -6,8 +6,8 @@ import com.openisle.mapper.ReactionMapper;
import com.openisle.model.Reaction;
import com.openisle.model.ReactionType;
import com.openisle.service.LevelService;
import com.openisle.service.PointService;
import com.openisle.service.ReactionService;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
@@ -20,6 +20,7 @@ public class ReactionController {
private final ReactionService reactionService;
private final LevelService levelService;
private final ReactionMapper reactionMapper;
private final PointService pointService;
/**
* Get all available reaction types.
@@ -39,6 +40,7 @@ public class ReactionController {
}
ReactionDto dto = reactionMapper.toDto(reaction);
dto.setReward(levelService.awardForReaction(auth.getName()));
pointService.awardForReactionOfPost(auth.getName(), postId);
return ResponseEntity.ok(dto);
}
@@ -52,6 +54,7 @@ public class ReactionController {
}
ReactionDto dto = reactionMapper.toDto(reaction);
dto.setReward(levelService.awardForReaction(auth.getName()));
pointService.awardForReactionOfComment(auth.getName(), commentId);
return ResponseEntity.ok(dto);
}
}

View File

@@ -17,5 +17,6 @@ public class CommentDto {
private List<CommentDto> replies;
private List<ReactionDto> reactions;
private int reward;
private int pointReward;
}

View File

@@ -27,5 +27,6 @@ public class PostSummaryDto {
private List<AuthorDto> participants;
private boolean subscribed;
private int reward;
private int pointReward;
}

View File

@@ -25,6 +25,7 @@ public class UserDto {
private long likesReceived;
private boolean subscribed;
private int experience;
private int point;
private int currentLevel;
private int nextLevelExp;
}

View File

@@ -53,6 +53,7 @@ public class UserMapper {
dto.setLikesSent(reactionService.countLikesSent(user.getUsername()));
dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername()));
dto.setExperience(user.getExperience());
dto.setPoint(user.getPoint());
dto.setCurrentLevel(levelService.getLevel(user.getExperience()));
dto.setNextLevelExp(levelService.nextLevelExp(user.getExperience()));
if (viewer != null) {

View File

@@ -0,0 +1,37 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDate;
/** Daily experience gain counts for a user. */
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "point_logs",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "log_date"}))
public class PointLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id")
private User user;
@Column(name = "log_date", nullable = false)
private LocalDate logDate;
@Column(name = "post_count", nullable = false)
private int postCount;
@Column(name = "comment_count", nullable = false)
private int commentCount;
@Column(name = "reaction_count", nullable = false)
private int reactionCount;
}

View File

@@ -5,9 +5,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import com.openisle.model.Role;
import java.time.LocalDateTime;
/**
* Simple user entity with basic fields and a role.
@@ -44,6 +43,9 @@ public class User {
@Column(nullable = false)
private int experience = 0;
@Column(nullable = false)
private int point = 0;
@Column(length = 1000)
private String introduction;

View File

@@ -0,0 +1,12 @@
package com.openisle.repository;
import com.openisle.model.PointLog;
import com.openisle.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.Optional;
public interface PointLogRepository extends JpaRepository<PointLog, Long> {
Optional<PointLog> findByUserAndLogDate(User user, LocalDate logDate);
}

View File

@@ -0,0 +1,119 @@
package com.openisle.service;
import com.openisle.model.PointLog;
import com.openisle.model.User;
import com.openisle.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
@Service
@RequiredArgsConstructor
public class PointService {
private final UserRepository userRepository;
private final PointLogRepository pointLogRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;
public int awardForPost(String userName) {
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);
}
private PointLog getTodayLog(User user) {
LocalDate today = LocalDate.now();
return pointLogRepository.findByUserAndLogDate(user, today)
.orElseGet(() -> {
PointLog log = new PointLog();
log.setUser(user);
log.setLogDate(today);
log.setPostCount(0);
log.setCommentCount(0);
log.setReactionCount(0);
return pointLogRepository.save(log);
});
}
private int addPoint(User user, int amount) {
user.setPoint(user.getPoint() + amount);
userRepository.save(user);
return amount;
}
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
// 注意需要考虑发帖和回复是同一人的场景
public int awardForComment(String commenterName, Long postId) {
// 标记评论者是否已达到积分奖励上限
boolean isTheRewardCapped = false;
// 根据帖子id找到发帖人
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
// 获取评论者的加分日志
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
PointLog log = getTodayLog(commenter);
if (log.getCommentCount() > 3) {
isTheRewardCapped = true;
}
// 如果发帖人与评论者是同一个,则只计算单次加分
if (poster.getId().equals(commenter.getId())) {
if (isTheRewardCapped) {
return 0;
} else {
log.setCommentCount(log.getCommentCount() + 1);
pointLogRepository.save(log);
return addPoint(commenter, 10);
}
} else {
addPoint(poster, 10);
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
if (isTheRewardCapped) {
return 0;
} else {
return addPoint(commenter, 10);
}
}
}
// 需要考虑点赞者和发帖人是同一个的情况
public int awardForReactionOfPost(String reactionerName, Long postId) {
// 根据帖子id找到发帖人
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
// 获取点赞者信息
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
// 如果发帖人与点赞者是同一个,则不加分
if (poster.getId().equals(reactioner.getId())) {
return 0;
}
// 如果不是同一个,则为发帖人加分
return addPoint(poster, 10);
}
// 考虑点赞者和评论者是同一个的情况
public int awardForReactionOfComment(String reactionerName, Long commentId) {
// 根据帖子id找到评论者
User commenter = commentRepository.findById(commentId).orElseThrow().getAuthor();
// 获取点赞者信息
User reactioner = userRepository.findByUsername(reactionerName).orElseThrow();
// 如果评论者与点赞者是同一个,则不加分
if (commenter.getId().equals(reactioner.getId())) {
return 0;
}
// 如果不是同一个,则为发帖人加分
return addPoint(commenter, 10);
}
}

View File

@@ -28,7 +28,8 @@
class="post-submit"
:class="{ disabled: !isLogin }"
@click="submitPost"
>发布</div>
>发布
</div>
<div v-else class="post-submit-loading"><i class="fa-solid fa-spinner fa-spin"></i> 发布中...</div>
</div>
</div>
@@ -232,11 +233,19 @@ export default {
})
const data = await res.json()
if (res.ok) {
if (data.reward && data.reward > 0) {
toast.success(`发布成功,获得 ${data.reward} 经验值`)
const reward = Math.max(0, Number(data?.reward) || 0); // 经验值
const points = Math.max(0, Number(data?.pointReward) || 0); // 积分值
if (reward && points) {
toast.success(`发布成功,获得 ${reward} 经验值、${points} 积分值`);
} else if (reward) {
toast.success(`发布成功,获得 ${reward} 经验值`);
} else if (points) {
toast.success(`发布成功,获得 ${points} 积分值`);
} else {
toast.success('发布成功')
toast.success('发布成功');
}
if (data.id) {
window.location.href = `/posts/${data.id}`
}
@@ -251,7 +260,19 @@ export default {
isWaitingPosting.value = false
}
}
return { title, content, selectedCategory, selectedTags, submitPost, saveDraft, clearPost, isWaitingPosting, aiGenerate, isAiLoading, isLogin }
return {
title,
content,
selectedCategory,
selectedTags,
submitPost,
saveDraft,
clearPost,
isWaitingPosting,
aiGenerate,
isAiLoading,
isLogin
}
}
}
</script>
@@ -301,7 +322,6 @@ export default {
}
.post-clear {
color: var(--primary-color);
cursor: pointer;
@@ -329,6 +349,7 @@ export default {
.post-submit:hover {
background-color: var(--primary-color-hover);
}
.post-submit.disabled:hover {
background-color: var(--primary-color-disabled);
}

View File

@@ -123,11 +123,22 @@ import { hatch } from 'ldrs'
import {useRouter} from 'vue-router'
import {isMobile} from '../utils/screen'
import Dropdown from '../components/Dropdown.vue'
hatch.register()
export default {
name: 'PostPageView',
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ArticleCategory, ReactionsGroup, DropdownMenu, VueEasyLightbox, Dropdown },
components: {
CommentItem,
CommentEditor,
BaseTimeline,
ArticleTags,
ArticleCategory,
ReactionsGroup,
DropdownMenu,
VueEasyLightbox,
Dropdown
},
setup() {
const route = useRoute()
const postId = route.params.id
@@ -386,11 +397,20 @@ export default {
console.debug('Post comment response data', data)
await fetchComments()
clear()
if (data.reward && data.reward > 0) {
toast.success(`评论成功,获得 ${data.reward} 经验值`)
const reward = Math.max(0, Number(data?.reward) || 0) // 经验值
const points = Math.max(0, Number(data?.pointReward) || 0) // 积分值
if (reward && points) {
toast.success(`评论成功,获得 ${reward} 经验值、${points} 积分值`)
} else if (reward) {
toast.success(`评论成功,获得 ${reward} 经验值`)
} else if (points) {
toast.success(`评论成功,获得 ${points} 积分值`)
} else {
toast.success('评论成功')
}
} else if (res.status === 429) {
toast.error('评论过于频繁,请稍后再试')
} else {