From ab9749a111ac64eaa3d3ab8969c8e104d4f57d72 Mon Sep 17 00:00:00 2001 From: Chuyaoyuan Date: Wed, 13 May 2026 18:30:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=AF=E5=88=86/=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V10 migration: add score column, back-fill from activity counts, recompute level - IScoreService + ScoreServiceImpl: addScore() atomically updates score and level in DB - UsersRepository.addScore(): JPQL UPDATE with inline CASE level computation - Level tiers: 0新手(<50) 1学徒(50) 2熟手(200) 3达人(500) 4专家(1000) 5大神(2000+) - Score events: - Post discussion: +10 (UserPostController) - Post reply: +5 (UserPostController) - Discussion/post liked: +2 to author; unliked/undone: -2 (LikeCollectServiceImpl) - Score self-like guard: no points awarded when user votes on own content - user-card API: expose level + score fields - user.ftl: replace raw level number with color-coded Lv.N badge + 积分 stat - discussions.ftl + index.ftl hover cards: show level badge and score Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controller/UserPostController.java | 11 +++++ .../controller/api/RestMsgController.java | 4 +- .../java/com/yaoyuan/jiscuss/entity/User.java | 3 ++ .../com/yaoyuan/jiscuss/entity/UserInfo.java | 1 + .../jiscuss/repository/UsersRepository.java | 13 +++++ .../jiscuss/service/IScoreService.java | 42 ++++++++++++++++ .../service/impl/LikeCollectServiceImpl.java | 48 ++++++++++++++++++- .../service/impl/ScoreServiceImpl.java | 23 +++++++++ .../db/migration/V10__score_system.sql | 23 +++++++++ src/main/resources/templates/discussions.ftl | 9 +++- src/main/resources/templates/index.ftl | 9 +++- src/main/resources/templates/user.ftl | 13 ++++- 12 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/yaoyuan/jiscuss/service/IScoreService.java create mode 100644 src/main/java/com/yaoyuan/jiscuss/service/impl/ScoreServiceImpl.java create mode 100644 src/main/resources/db/migration/V10__score_system.sql diff --git a/src/main/java/com/yaoyuan/jiscuss/controller/UserPostController.java b/src/main/java/com/yaoyuan/jiscuss/controller/UserPostController.java index cf63d24..2a127d0 100644 --- a/src/main/java/com/yaoyuan/jiscuss/controller/UserPostController.java +++ b/src/main/java/com/yaoyuan/jiscuss/controller/UserPostController.java @@ -74,6 +74,9 @@ public class UserPostController extends BaseController { @Autowired private com.yaoyuan.jiscuss.service.INotificationService notificationService; + @Autowired + private com.yaoyuan.jiscuss.service.IScoreService scoreService; + /** * 首页查看主题列表(各种条件筛选,最热最新标签等) @@ -183,6 +186,10 @@ public class UserPostController extends BaseController { logger.info(">>>{}", saveDiscussion); resultobj.put("msg", "添加主题成功"); resultobj.put("flag", true); + // Award points for creating a discussion + if (user != null) { + scoreService.addScore(user.getId(), 10); + } return resultobj; } @@ -242,6 +249,10 @@ public class UserPostController extends BaseController { logger.info(">>>{}", savePost); resultobj.put("msg", "添加评论成功"); resultobj.put("flag", true); + // Award points for posting a reply + if (user != null) { + scoreService.addScore(user.getId(), 5); + } return resultobj; } diff --git a/src/main/java/com/yaoyuan/jiscuss/controller/api/RestMsgController.java b/src/main/java/com/yaoyuan/jiscuss/controller/api/RestMsgController.java index 29d3597..70adc53 100644 --- a/src/main/java/com/yaoyuan/jiscuss/controller/api/RestMsgController.java +++ b/src/main/java/com/yaoyuan/jiscuss/controller/api/RestMsgController.java @@ -49,7 +49,9 @@ public class RestMsgController extends BaseController { "id", user.getId(), "username", user.getUsername() != null ? user.getUsername() : "", "discussionsCount", user.getDiscussionsCount() != null ? user.getDiscussionsCount() : 0, - "commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0 + "commentsCount", user.getCommentsCount() != null ? user.getCommentsCount() : 0, + "level", user.getLevel() != null ? user.getLevel() : 0, + "score", user.getScore() != null ? user.getScore() : 0 )); } diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/User.java b/src/main/java/com/yaoyuan/jiscuss/entity/User.java index 185a2ae..d85543b 100644 --- a/src/main/java/com/yaoyuan/jiscuss/entity/User.java +++ b/src/main/java/com/yaoyuan/jiscuss/entity/User.java @@ -76,4 +76,7 @@ public class User implements Serializable { @Column(name = "level") private Integer level; + + @Column(name = "score") + private Integer score; } diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/UserInfo.java b/src/main/java/com/yaoyuan/jiscuss/entity/UserInfo.java index 5b57617..42f7b2f 100644 --- a/src/main/java/com/yaoyuan/jiscuss/entity/UserInfo.java +++ b/src/main/java/com/yaoyuan/jiscuss/entity/UserInfo.java @@ -26,6 +26,7 @@ public class UserInfo implements UserDetails { private String email; private String gender; private Integer level; + private Integer score; private Integer flag; public UserInfo() { diff --git a/src/main/java/com/yaoyuan/jiscuss/repository/UsersRepository.java b/src/main/java/com/yaoyuan/jiscuss/repository/UsersRepository.java index 347d6db..f37d5bf 100644 --- a/src/main/java/com/yaoyuan/jiscuss/repository/UsersRepository.java +++ b/src/main/java/com/yaoyuan/jiscuss/repository/UsersRepository.java @@ -48,4 +48,17 @@ public interface UsersRepository extends JpaRepository { /** Top active users by comment count (for the ranking sidebar). Shows all users if counts are 0. */ @Query("FROM User u ORDER BY COALESCE(u.commentsCount, 0) DESC") List findTop10ActiveUsers(org.springframework.data.domain.Pageable pageable); + + @Modifying + @Transactional + @Query("UPDATE User u SET u.score = GREATEST(0, COALESCE(u.score, 0) + :delta), " + + "u.level = CASE " + + " WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 2000 THEN 5 " + + " WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 1000 THEN 4 " + + " WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 500 THEN 3 " + + " WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 200 THEN 2 " + + " WHEN (GREATEST(0, COALESCE(u.score, 0) + :delta)) >= 50 THEN 1 " + + " ELSE 0 END " + + "WHERE u.id = :id") + void addScore(@Param("id") Integer id, @Param("delta") int delta); } diff --git a/src/main/java/com/yaoyuan/jiscuss/service/IScoreService.java b/src/main/java/com/yaoyuan/jiscuss/service/IScoreService.java new file mode 100644 index 0000000..9e584e0 --- /dev/null +++ b/src/main/java/com/yaoyuan/jiscuss/service/IScoreService.java @@ -0,0 +1,42 @@ +package com.yaoyuan.jiscuss.service; + +/** + * Service for awarding activity points and recomputing user levels. + * + *

Level thresholds (score → level): + *

    + *
  • 0 – 49 → 0 新手
  • + *
  • 50 – 199 → 1 学徒
  • + *
  • 200 – 499 → 2 熟手
  • + *
  • 500 – 999 → 3 达人
  • + *
  • 1000 – 1999 → 4 专家
  • + *
  • 2000+ → 5 大神
  • + *
+ */ +public interface IScoreService { + + /** Award (or deduct) {@code delta} points to the user and refresh their level. */ + void addScore(Integer userId, int delta); + + /** Compute the level (0-5) corresponding to a given score. */ + static int computeLevel(int score) { + if (score >= 2000) return 5; + if (score >= 1000) return 4; + if (score >= 500) return 3; + if (score >= 200) return 2; + if (score >= 50) return 1; + return 0; + } + + /** Human-readable name for a level (0-5). */ + static String levelName(int level) { + return switch (level) { + case 1 -> "学徒"; + case 2 -> "熟手"; + case 3 -> "达人"; + case 4 -> "专家"; + case 5 -> "大神"; + default -> "新手"; + }; + } +} diff --git a/src/main/java/com/yaoyuan/jiscuss/service/impl/LikeCollectServiceImpl.java b/src/main/java/com/yaoyuan/jiscuss/service/impl/LikeCollectServiceImpl.java index c5c28fb..0e93ead 100644 --- a/src/main/java/com/yaoyuan/jiscuss/service/impl/LikeCollectServiceImpl.java +++ b/src/main/java/com/yaoyuan/jiscuss/service/impl/LikeCollectServiceImpl.java @@ -7,6 +7,7 @@ import com.yaoyuan.jiscuss.repository.LikeCollectRepository; import com.yaoyuan.jiscuss.repository.PostsRepository; import com.yaoyuan.jiscuss.service.ILikeCollectService; import com.yaoyuan.jiscuss.service.INotificationService; +import com.yaoyuan.jiscuss.entity.Discussion; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,9 +30,19 @@ public class LikeCollectServiceImpl implements ILikeCollectService { @Autowired private INotificationService notificationService; + @Autowired + private com.yaoyuan.jiscuss.service.IScoreService scoreService; + + @Autowired + private com.yaoyuan.jiscuss.repository.UsersRepository usersRepository; + @Transactional @Override public String voteDiscussion(Integer userId, Integer discussionId, String action) { + // Look up discussion author once for score updates + com.yaoyuan.jiscuss.entity.Discussion disc = discussionsRepository.findById(discussionId).orElse(null); + Integer authorId = (disc != null) ? disc.getStartUserId() : null; + Optional existing = likeCollectRepository.findDiscussionVote(userId, discussionId); if (existing.isPresent()) { LikeCollect vote = existing.get(); @@ -39,6 +50,10 @@ public class LikeCollectServiceImpl implements ILikeCollectService { // Toggle off: cancel the same action likeCollectRepository.delete(vote); adjustDiscussion(discussionId, action, -1); + // Reverse score for the author (only likes affect score) + if ("like".equals(action) && authorId != null && !authorId.equals(userId)) { + scoreService.addScore(authorId, -2); + } return "none"; } else { // Switch action: undo old, apply new @@ -46,6 +61,14 @@ public class LikeCollectServiceImpl implements ILikeCollectService { vote.setAction(action); likeCollectRepository.save(vote); adjustDiscussion(discussionId, action, +1); + // Switched from like→dislike: deduct; dislike→like: award + if (authorId != null && !authorId.equals(userId)) { + if ("like".equals(action)) { + scoreService.addScore(authorId, +2); + } else { + scoreService.addScore(authorId, -2); + } + } return action; } } else { @@ -59,6 +82,10 @@ public class LikeCollectServiceImpl implements ILikeCollectService { vote.setCreateTime(new Date()); likeCollectRepository.save(vote); adjustDiscussion(discussionId, action, +1); + // Award score to discussion author on new like + if ("like".equals(action) && authorId != null && !authorId.equals(userId)) { + scoreService.addScore(authorId, +2); + } return action; } } @@ -72,12 +99,28 @@ public class LikeCollectServiceImpl implements ILikeCollectService { if (vote.getAction().equals(action)) { likeCollectRepository.delete(vote); adjustPost(postId, action, -1); + // Reverse score on unlike + if ("like".equals(action)) { + Post post = postsRepository.findById(postId).orElse(null); + if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) { + scoreService.addScore(post.getCreateId(), -2); + } + } return "none"; } else { adjustPost(postId, vote.getAction(), -1); vote.setAction(action); likeCollectRepository.save(vote); adjustPost(postId, action, +1); + // Switched vote direction: adjust score accordingly + Post post = postsRepository.findById(postId).orElse(null); + if (post != null && post.getCreateId() != null && !post.getCreateId().equals(userId)) { + if ("like".equals(action)) { + scoreService.addScore(post.getCreateId(), +2); + } else { + scoreService.addScore(post.getCreateId(), -2); + } + } return action; } } else { @@ -90,11 +133,14 @@ public class LikeCollectServiceImpl implements ILikeCollectService { vote.setCreateTime(new Date()); likeCollectRepository.save(vote); adjustPost(postId, action, +1); - // Notify post author when liked (not disliked) + // Award score to post author on new like; also send notification if ("like".equals(action)) { Post post = postsRepository.findById(postId).orElse(null); if (post != null && post.getCreateId() != null) { notificationService.notifyLike(post.getCreateId(), userId, postId, post.getDiscussionId()); + if (!post.getCreateId().equals(userId)) { + scoreService.addScore(post.getCreateId(), +2); + } } } return action; diff --git a/src/main/java/com/yaoyuan/jiscuss/service/impl/ScoreServiceImpl.java b/src/main/java/com/yaoyuan/jiscuss/service/impl/ScoreServiceImpl.java new file mode 100644 index 0000000..7551bae --- /dev/null +++ b/src/main/java/com/yaoyuan/jiscuss/service/impl/ScoreServiceImpl.java @@ -0,0 +1,23 @@ +package com.yaoyuan.jiscuss.service.impl; + +import com.yaoyuan.jiscuss.repository.UsersRepository; +import com.yaoyuan.jiscuss.service.IScoreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ScoreServiceImpl implements IScoreService { + + @Autowired + private UsersRepository usersRepository; + + @Override + @Transactional + @CacheEvict(value = "user", key = "#userId") + public void addScore(Integer userId, int delta) { + if (userId == null || delta == 0) return; + usersRepository.addScore(userId, delta); + } +} diff --git a/src/main/resources/db/migration/V10__score_system.sql b/src/main/resources/db/migration/V10__score_system.sql new file mode 100644 index 0000000..5652e6d --- /dev/null +++ b/src/main/resources/db/migration/V10__score_system.sql @@ -0,0 +1,23 @@ +-- V10: Add score column for the points/level system. +-- score: cumulative activity points (default 0). +-- level is already present; it will be recomputed by ScoreServiceImpl whenever score changes. +ALTER TABLE user ADD COLUMN score INTEGER NOT NULL DEFAULT 0; + +-- Back-fill an initial score for existing users based on their activity counts. +-- Formula: discussions_count*10 + comments_count*5 (mirrors live award rates) +UPDATE user +SET score = COALESCE(discussions_count, 0) * 10 + COALESCE(comments_count, 0) * 5; + +-- Recompute level for all users based on back-filled score. +-- Thresholds: 0=新手(<50), 1=学徒(50-199), 2=熟手(200-499), +-- 3=达人(500-999), 4=专家(1000-1999), 5=大神(>=2000) +UPDATE user SET level = + CASE + WHEN score >= 2000 THEN 5 + WHEN score >= 1000 THEN 4 + WHEN score >= 500 THEN 3 + WHEN score >= 200 THEN 2 + WHEN score >= 50 THEN 1 + ELSE 0 + END +WHERE level = 0 OR level IS NULL; diff --git a/src/main/resources/templates/discussions.ftl b/src/main/resources/templates/discussions.ftl index 86945d2..507ccc7 100644 --- a/src/main/resources/templates/discussions.ftl +++ b/src/main/resources/templates/discussions.ftl @@ -287,10 +287,15 @@ var $card = $('').appendTo('body'); function showCard(u, x, y) { + var lvlNames = ['新手','学徒','熟手','达人','专家','大神']; + var lvlColors = ['#95a5a6','#27ae60','#2980b9','#8e44ad','#e67e22','#e74c3c']; + var lvl = Math.min(5, Math.max(0, u.level || 0)); + var badge = 'Lv.' + lvl + ' ' + lvlNames[lvl] + ''; $card.html( - '
' + (u.username || '') + '
' + + '
' + (u.username || '') + ' ' + badge + '
' + '
发帖: ' + (u.discussionsCount || 0) + - '  回复: ' + (u.commentsCount || 0) + '
' + + '  回复: ' + (u.commentsCount || 0) + + '  积分: ' + (u.score || 0) + '' + '
' + '主页' + '发私信' + diff --git a/src/main/resources/templates/index.ftl b/src/main/resources/templates/index.ftl index 4563795..6788999 100644 --- a/src/main/resources/templates/index.ftl +++ b/src/main/resources/templates/index.ftl @@ -564,10 +564,15 @@ var cardCache = {}; var $card = $('').appendTo('body'); function showCard(u, x, y) { + var lvlNames = ['新手','学徒','熟手','达人','专家','大神']; + var lvlColors = ['#95a5a6','#27ae60','#2980b9','#8e44ad','#e67e22','#e74c3c']; + var lvl = Math.min(5, Math.max(0, u.level || 0)); + var badge = 'Lv.' + lvl + ' ' + lvlNames[lvl] + ''; $card.html( - '
' + (u.username || '') + '
' + + '
' + (u.username || '') + ' ' + badge + '
' + '
发帖: ' + (u.discussionsCount || 0) + - '  回复: ' + (u.commentsCount || 0) + '
' + + '  回复: ' + (u.commentsCount || 0) + + '  积分: ' + (u.score || 0) + '
' + '
' + '主页' + '发私信' + diff --git a/src/main/resources/templates/user.ftl b/src/main/resources/templates/user.ftl index 8d75cb6..5a17f34 100644 --- a/src/main/resources/templates/user.ftl +++ b/src/main/resources/templates/user.ftl @@ -104,10 +104,19 @@
回复
-
${(userEntity.level)!1}
-
等级
+
${(userEntity.score)!0}
+
积分
+
+ <#assign lvl = (userEntity.level)!0> + <#assign lvlName = ["新手","学徒","熟手","达人","专家","大神"][lvl?int]> + <#assign lvlColors = ["#95a5a6","#27ae60","#2980b9","#8e44ad","#e67e22","#e74c3c"]> + + Lv.${lvl} ${lvlName} + +