update theme and user top

This commit is contained in:
2026-05-12 18:49:34 +08:00
parent ba8c1e3be3
commit 4dfd27c785
32 changed files with 777 additions and 78 deletions
@@ -50,6 +50,14 @@ public class Node {
private String ipAddress;
private String ipRegion;
private String browser;
private Integer likeCount;
private Integer dislikeCount;
private String copyright;
private Integer isApproved;
@@ -52,6 +52,33 @@ public class PostCommonUtil {
postCustom.setAvatar(avatar);
postCustom.setUsername(mapObj.get("create_username") != null ? String.valueOf(mapObj.get("create_username")) : null);
postCustom.setRealname(mapObj.get("create_realname") != null ? String.valueOf(mapObj.get("create_realname")) : null);
// floor number
if (mapObj.get("number") != null) {
postCustom.setNumber(Integer.parseInt(String.valueOf(mapObj.get("number"))));
}
// create user id
if (mapObj.get("create_id") != null) {
postCustom.setCreateId(Integer.parseInt(String.valueOf(mapObj.get("create_id"))));
}
// ip address
if (mapObj.get("ip_address") != null) {
postCustom.setIpAddress(String.valueOf(mapObj.get("ip_address")));
}
// ip region (may be null if column not yet populated)
if (mapObj.get("ip_region") != null) {
postCustom.setIpRegion(String.valueOf(mapObj.get("ip_region")));
}
// browser info (user_agent stored but only display-parsed browser is used)
if (mapObj.get("browser") != null) {
postCustom.setBrowser(String.valueOf(mapObj.get("browser")));
}
// vote counters
if (mapObj.get("like_count") != null) {
postCustom.setLikeCount(Integer.parseInt(String.valueOf(mapObj.get("like_count"))));
}
if (mapObj.get("dislike_count") != null) {
postCustom.setDislikeCount(Integer.parseInt(String.valueOf(mapObj.get("dislike_count"))));
}
String avatarReply = "";
if (mapObj.get("user_avatar") != null) {
if (mapObj.get("user_avatar") instanceof Clob) {
@@ -12,6 +12,9 @@ import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
import com.yaoyuan.jiscuss.service.IPostsService;
import com.yaoyuan.jiscuss.service.ITagsService;
import com.yaoyuan.jiscuss.service.IUsersService;
import com.yaoyuan.jiscuss.service.IpRegionService;
import com.yaoyuan.jiscuss.util.IpUtils;
import com.yaoyuan.jiscuss.util.UserAgentUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
@@ -56,6 +59,9 @@ public class UserPostController extends BaseController {
@Autowired
private IUsersService usersService;
@Autowired
private IpRegionService ipRegionService;
/**
* 首页查看主题列表(各种条件筛选,最热最新标签等)
@@ -70,8 +76,10 @@ public class UserPostController extends BaseController {
* @return
*/
@RequestMapping("/getdiscussionsbyid")
public String getDiscussionsById(HttpServletRequest request, ModelMap map, @RequestParam("id") Integer id) {
logger.info(">>> getDiscussionsById{}", id);
public String getDiscussionsById(HttpServletRequest request, ModelMap map,
@RequestParam("id") Integer id,
@RequestParam(defaultValue = "newest") String sort) {
logger.info(">>> getDiscussionsById{} sort={}", id, sort);
// Atomically increment view count before loading the discussion
discussionsService.incrementViewCount(id);
@@ -96,10 +104,11 @@ public class UserPostController extends BaseController {
}
List<Tag> tags = tagsService.findByDId(id);
List postsObj = postsService.findPostCustomById(id);
List postsObj = postsService.findPostCustomById(id, sort);
map.put("tags", tags);
map.put("discussions", newdd);
map.put("posts", postsObj);
map.put("sort", sort);
UserInfo user = getUserInfo(request);
if (user != null) {
map.put("username", user.getUsername());
@@ -183,6 +192,13 @@ public class UserPostController extends BaseController {
post.setCreateId(user.getId());
}
post.setCreateTime(new Date());
// Capture client IP, region, and browser
String clientIp = IpUtils.getClientIp(request);
post.setIpAddress(clientIp);
post.setIpRegion(ipRegionService.getRegion(clientIp));
post.setBrowser(UserAgentUtils.parseBrowser(request.getHeader("User-Agent")));
if (null != post.getParentId()) {
Post temp = postsService.findOneByid(post.getParentId());
post.setUserId(temp.getCreateId());
@@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
@@ -50,6 +51,9 @@ public class UserSystemController extends BaseController {
@Autowired
private ITagsService tagsService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 首页index
* @param tag
@@ -113,6 +117,7 @@ public class UserSystemController extends BaseController {
map.put("pageNum", pageNum);
map.put("pageTotalPages", allDiscussionsPage.getTotalPages());
map.put("pageNumAll", allDiscussionsPage.getTotalPages());
map.put("topUsers", usersService.getTopActiveUsers(10));
UserInfo user = getUserInfo(request);
if (user != null) {
map.put("username", user.getUsername());
@@ -251,7 +256,7 @@ public class UserSystemController extends BaseController {
} else {
User user = new User();
user.setEmail(email);
user.setPassword(password);
user.setPassword(passwordEncoder.encode(password));
user.setUsername(username);
user.setRealname(username);
user.setJoinTime(new Date());
@@ -0,0 +1,63 @@
package com.yaoyuan.jiscuss.controller.api;
import com.yaoyuan.jiscuss.controller.BaseController;
import com.yaoyuan.jiscuss.entity.UserInfo;
import com.yaoyuan.jiscuss.response.ApiResponse;
import com.yaoyuan.jiscuss.service.ILikeCollectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* REST API for voting (like / dislike) on discussions and posts.
*/
@Tag(name = "Vote API", description = "Like and dislike discussions and posts")
@RestController
@RequestMapping("/api")
public class VoteController extends BaseController {
@Autowired
private ILikeCollectService likeCollectService;
@Operation(summary = "Vote on a discussion")
@PostMapping("/discussions/{id}/vote")
public ApiResponse<Map<String, String>> voteDiscussion(
@PathVariable Integer id,
@RequestParam String action,
HttpServletRequest request) {
UserInfo user = getUserInfo(request);
if (user == null) {
return ApiResponse.fail(401, "请先登录");
}
if (!"like".equals(action) && !"dislike".equals(action)) {
return ApiResponse.fail(400, "action 参数必须是 like 或 dislike");
}
String result = likeCollectService.voteDiscussion(user.getId(), id, action);
return ApiResponse.ok(Map.of("action", result));
}
@Operation(summary = "Vote on a post (reply)")
@PostMapping("/posts/{id}/vote")
public ApiResponse<Map<String, String>> votePost(
@PathVariable Integer id,
@RequestParam String action,
HttpServletRequest request) {
UserInfo user = getUserInfo(request);
if (user == null) {
return ApiResponse.fail(401, "请先登录");
}
if (!"like".equals(action) && !"dislike".equals(action)) {
return ApiResponse.fail(400, "action 参数必须是 like 或 dislike");
}
String result = likeCollectService.votePost(user.getId(), id, action);
return ApiResponse.ok(Map.of("action", result));
}
}
@@ -68,6 +68,9 @@ public class Discussion implements Serializable {
@Column(name = "like_count")
private Integer likeCount;
@Column(name = "dislike_count")
private Integer dislikeCount;
/** IP is resolved server-side via IpUtils.getClientIp() — never trusted from X-Forwarded-For alone. */
@Column(name = "ip_address")
private String ipAddress;
@@ -48,6 +48,10 @@ public class LikeCollect implements Serializable {
@Column(name = "type")
private String type;
/** Vote action: 'like' or 'dislike'. */
@Column(name = "action")
private String action;
@Column(name = "like_type")
private String likeType;
@@ -54,12 +54,26 @@ public class Post implements Serializable {
@Column(name = "ip_address")
private String ipAddress;
/** Province/region derived from IP (e.g. "北京", "广东"). Stored on post creation. */
@Column(name = "ip_region")
private String ipRegion;
/** Parsed browser name+version (e.g. "Chrome 120"). Derived from User-Agent on post creation. */
@Column(name = "browser")
private String browser;
@Column(name = "copyright")
private String copyright;
@Column(name = "is_approved")
private Integer isApproved;
@Column(name = "like_count")
private Integer likeCount;
@Column(name = "dislike_count")
private Integer dislikeCount;
@Column(name = "create_id")
private Integer createId;
@@ -9,6 +9,8 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
@Repository
public interface DiscussionsRepository extends JpaRepository<Discussion, Integer> {
@@ -20,4 +22,24 @@ public interface DiscussionsRepository extends JpaRepository<Discussion, Integer
@Transactional
@Query("UPDATE Discussion d SET d.viewCount = d.viewCount + 1 WHERE d.id = :id")
void incrementViewCount(@org.springframework.data.repository.query.Param("id") Integer id);
@Modifying
@Transactional
@Query("UPDATE Discussion d SET d.commentsCount = COALESCE(d.commentsCount, 0) + 1, d.lastTime = :lastTime, d.lastUserId = :userId WHERE d.id = :id")
void incrementCommentCount(
@org.springframework.data.repository.query.Param("id") Integer id,
@org.springframework.data.repository.query.Param("userId") Integer userId,
@org.springframework.data.repository.query.Param("lastTime") Date lastTime);
@Modifying
@Transactional
@Query("UPDATE Discussion d SET d.likeCount = COALESCE(d.likeCount, 0) + :delta WHERE d.id = :id")
void adjustLikeCount(@org.springframework.data.repository.query.Param("id") Integer id,
@org.springframework.data.repository.query.Param("delta") int delta);
@Modifying
@Transactional
@Query("UPDATE Discussion d SET d.dislikeCount = COALESCE(d.dislikeCount, 0) + :delta WHERE d.id = :id")
void adjustDislikeCount(@org.springframework.data.repository.query.Param("id") Integer id,
@org.springframework.data.repository.query.Param("delta") int delta);
}
@@ -0,0 +1,21 @@
package com.yaoyuan.jiscuss.repository;
import com.yaoyuan.jiscuss.entity.LikeCollect;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface LikeCollectRepository extends JpaRepository<LikeCollect, Integer> {
/** Find an existing vote record for a user on a discussion. */
@Query("FROM LikeCollect lc WHERE lc.userId = :userId AND lc.discussionId = :discussionId AND lc.type = 'discussion'")
Optional<LikeCollect> findDiscussionVote(@Param("userId") Integer userId, @Param("discussionId") Integer discussionId);
/** Find an existing vote record for a user on a post. */
@Query("FROM LikeCollect lc WHERE lc.userId = :userId AND lc.postId = :postId AND lc.type = 'post'")
Optional<LikeCollect> findPostVote(@Param("userId") Integer userId, @Param("postId") Integer postId);
}
@@ -2,9 +2,11 @@ package com.yaoyuan.jiscuss.repository;
import com.yaoyuan.jiscuss.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
@@ -21,6 +23,20 @@ public interface PostsRepository extends JpaRepository<Post, Integer> {
@Query("from Post where discussionId = :dId")
List<Post> findOneBy(@Param("dId") Integer dId);
/** Returns the current max floor number for a discussion (0 if no posts yet). */
@Query("SELECT COALESCE(MAX(p.number), 0) FROM Post p WHERE p.discussionId = :dId")
int findMaxNumberByDiscussionId(@Param("dId") Integer dId);
@Modifying
@Transactional
@Query("UPDATE Post p SET p.likeCount = COALESCE(p.likeCount, 0) + :delta WHERE p.id = :id")
void adjustLikeCount(@Param("id") Integer id, @Param("delta") int delta);
@Modifying
@Transactional
@Query("UPDATE Post p SET p.dislikeCount = COALESCE(p.dislikeCount, 0) + :delta WHERE p.id = :id")
void adjustDislikeCount(@Param("id") Integer id, @Param("delta") int delta);
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
"from post p \n" +
@@ -31,6 +47,15 @@ public interface PostsRepository extends JpaRepository<Post, Integer> {
List<Map<String, Object>> findPostCustomById(@Param("dId") Integer dId);
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
"from post p \n" +
"left join user u on p.user_id = u.id \n" +
"left join user u2 on p.create_id = u2.id \n" +
"where p.discussion_id = :dId and p.parent_id is null order by p.create_time asc"
, nativeQuery = true)
List<Map<String, Object>> findAllByDIdAndparentIdNullOldest(@Param("dId") Integer dId);
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
"from post p \n" +
@@ -38,7 +63,16 @@ public interface PostsRepository extends JpaRepository<Post, Integer> {
"left join user u2 on p.create_id = u2.id \n" +
"where p.discussion_id = :dId and p.parent_id is null order by p.create_time desc"
, nativeQuery = true)
List<Map<String, Object>> findAllByDIdAndparentIdNull(@Param("dId") Integer dId);
List<Map<String, Object>> findAllByDIdAndparentIdNullNewest(@Param("dId") Integer dId);
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
"from post p \n" +
"left join user u on p.user_id = u.id \n" +
"left join user u2 on p.create_id = u2.id \n" +
"where p.discussion_id = :dId and p.parent_id is null order by p.like_count desc, p.create_time desc"
, nativeQuery = true)
List<Map<String, Object>> findAllByDIdAndparentIdNullHot(@Param("dId") Integer dId);
@Query(value = "select p.*,u.avatar as user_avatar ,u.username as user_username ,u.realname as user_realname ," +
"u2.avatar as create_avatar ,u2.username as create_username ,u2.realname as create_realname \n" +
@@ -2,9 +2,11 @@ package com.yaoyuan.jiscuss.repository;
import com.yaoyuan.jiscuss.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@@ -32,4 +34,18 @@ public interface UsersRepository extends JpaRepository<User, Integer> {
// REMOVED: checkByUsernameAndPassword — never compare passwords at the SQL layer.
// Password validation must go through BCryptPasswordEncoder.matches() in the service/security layer.
@Modifying
@Transactional
@Query("UPDATE User u SET u.commentsCount = COALESCE(u.commentsCount, 0) + 1 WHERE u.id = :id")
void incrementCommentCount(@Param("id") Integer id);
@Modifying
@Transactional
@Query("UPDATE User u SET u.discussionsCount = COALESCE(u.discussionsCount, 0) + 1 WHERE u.id = :id")
void incrementDiscussionCount(@Param("id") Integer id);
/** 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<User> findTop10ActiveUsers(org.springframework.data.domain.Pageable pageable);
}
@@ -15,6 +15,10 @@ public record ApiResponse<T>(int code, String msg, T data) {
return new ApiResponse<>(responseCode.getCode(), responseCode.getMsg(), null);
}
public static <T> ApiResponse<T> fail(int code, String msg) {
return new ApiResponse<>(code, msg, null);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(ResponseCode.SERVICE_ERROR.getCode(), message, null);
}
@@ -0,0 +1,22 @@
package com.yaoyuan.jiscuss.service;
/**
* Vote (like / dislike) service for discussions and posts.
*
* <p>Toggle semantics:
* <ul>
* <li>If the user has not voted → create a new vote with the requested action.</li>
* <li>If the user voted with the same action → cancel the vote (toggle off).</li>
* <li>If the user voted with a different action → switch the vote.</li>
* </ul>
*
* @return the user's current action after the operation: "like", "dislike", or "none" (cancelled)
*/
public interface ILikeCollectService {
/** Vote on a discussion. {@code action} must be "like" or "dislike". */
String voteDiscussion(Integer userId, Integer discussionId, String action);
/** Vote on a post (reply). {@code action} must be "like" or "dislike". */
String votePost(Integer userId, Integer postId, String action);
}
@@ -12,7 +12,7 @@ public interface IPostsService {
List<Post> findOneBy(Integer id);
List<PostCustom> findPostCustomById(Integer id);
List findPostCustomById(Integer id, String sort);
Post findOneByid(Integer parentId);
}
@@ -27,4 +27,6 @@ public interface IUsersService {
List<User> findAllById(Iterable<Integer> ids);
User update(User user, Integer id);
List<User> getTopActiveUsers(int limit);
}
@@ -0,0 +1,71 @@
package com.yaoyuan.jiscuss.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Set;
/**
* Lightweight IP region service.
*
* <p>Current implementation distinguishes local/private addresses from external ones.
* To display accurate province-level location (e.g. "北京", "广东"), replace the
* {@code lookupExternal} method with an ip2region XDB lookup or similar offline database.
*
* <p>Upgrade path:
* <ol>
* <li>Add {@code org.lionsoul:ip2region:2.7.0} to pom.xml</li>
* <li>Place {@code ip2region.xdb} (from the ip2region GitHub release) under
* {@code src/main/resources/}</li>
* <li>Replace {@code lookupExternal()} with:
* {@code Searcher.newWithFileOnly(xdbPath).searchByStr(ip)}</li>
* </ol>
*/
@Service
public class IpRegionService {
private static final Logger log = LoggerFactory.getLogger(IpRegionService.class);
private static final Set<String> LOCAL_PREFIXES = Set.of(
"127.", "0.", "::1", "0:0:0:0:0:0:0:1"
);
private static final Set<String> PRIVATE_PREFIXES = Set.of(
"10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
"172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31."
);
/**
* Returns a display-friendly region label for the given IP address.
*
* @param ip client IP string (IPv4 or IPv6)
* @return region label, e.g. "本地", "局域网", or "未知地区"
*/
public String getRegion(String ip) {
if (ip == null || ip.isBlank()) {
return "";
}
for (String prefix : LOCAL_PREFIXES) {
if (ip.startsWith(prefix)) {
return "本地";
}
}
for (String prefix : PRIVATE_PREFIXES) {
if (ip.startsWith(prefix)) {
return "局域网";
}
}
return lookupExternal(ip);
}
/**
* Override this method to integrate an offline IP database (e.g. ip2region).
* Default implementation returns "未知地区" as a placeholder.
*/
protected String lookupExternal(String ip) {
// TODO: integrate ip2region XDB for accurate province-level lookup
return "未知地区";
}
}
@@ -2,6 +2,7 @@ package com.yaoyuan.jiscuss.service.impl;
import com.yaoyuan.jiscuss.entity.Discussion;
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
import com.yaoyuan.jiscuss.repository.UsersRepository;
import com.yaoyuan.jiscuss.service.IDiscussionsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
@@ -20,6 +21,9 @@ public class DiscussionsServiceImpl implements IDiscussionsService {
@Autowired
private DiscussionsRepository discussionsRepository;
@Autowired
private UsersRepository usersRepository;
@Transactional(readOnly = true)
@Override
public List<Discussion> getAllList() {
@@ -29,7 +33,12 @@ public class DiscussionsServiceImpl implements IDiscussionsService {
@Transactional
@Override
public Discussion insert(Discussion discussion) {
return discussionsRepository.save(discussion);
Discussion saved = discussionsRepository.save(discussion);
// Maintain user discussions count
if (discussion.getCreateId() != null) {
usersRepository.incrementDiscussionCount(discussion.getCreateId());
}
return saved;
}
@Transactional(readOnly = true)
@@ -0,0 +1,107 @@
package com.yaoyuan.jiscuss.service.impl;
import com.yaoyuan.jiscuss.entity.LikeCollect;
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
import com.yaoyuan.jiscuss.repository.LikeCollectRepository;
import com.yaoyuan.jiscuss.repository.PostsRepository;
import com.yaoyuan.jiscuss.service.ILikeCollectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Optional;
@Service
public class LikeCollectServiceImpl implements ILikeCollectService {
@Autowired
private LikeCollectRepository likeCollectRepository;
@Autowired
private DiscussionsRepository discussionsRepository;
@Autowired
private PostsRepository postsRepository;
@Transactional
@Override
public String voteDiscussion(Integer userId, Integer discussionId, String action) {
Optional<LikeCollect> existing = likeCollectRepository.findDiscussionVote(userId, discussionId);
if (existing.isPresent()) {
LikeCollect vote = existing.get();
if (vote.getAction().equals(action)) {
// Toggle off: cancel the same action
likeCollectRepository.delete(vote);
adjustDiscussion(discussionId, action, -1);
return "none";
} else {
// Switch action: undo old, apply new
adjustDiscussion(discussionId, vote.getAction(), -1);
vote.setAction(action);
likeCollectRepository.save(vote);
adjustDiscussion(discussionId, action, +1);
return action;
}
} else {
// New vote
LikeCollect vote = new LikeCollect();
vote.setUserId(userId);
vote.setDiscussionId(discussionId);
vote.setType("discussion");
vote.setAction(action);
vote.setCreateId(userId);
vote.setCreateTime(new Date());
likeCollectRepository.save(vote);
adjustDiscussion(discussionId, action, +1);
return action;
}
}
@Transactional
@Override
public String votePost(Integer userId, Integer postId, String action) {
Optional<LikeCollect> existing = likeCollectRepository.findPostVote(userId, postId);
if (existing.isPresent()) {
LikeCollect vote = existing.get();
if (vote.getAction().equals(action)) {
likeCollectRepository.delete(vote);
adjustPost(postId, action, -1);
return "none";
} else {
adjustPost(postId, vote.getAction(), -1);
vote.setAction(action);
likeCollectRepository.save(vote);
adjustPost(postId, action, +1);
return action;
}
} else {
LikeCollect vote = new LikeCollect();
vote.setUserId(userId);
vote.setPostId(postId);
vote.setType("post");
vote.setAction(action);
vote.setCreateId(userId);
vote.setCreateTime(new Date());
likeCollectRepository.save(vote);
adjustPost(postId, action, +1);
return action;
}
}
private void adjustDiscussion(Integer id, String action, int delta) {
if ("like".equals(action)) {
discussionsRepository.adjustLikeCount(id, delta);
} else {
discussionsRepository.adjustDislikeCount(id, delta);
}
}
private void adjustPost(Integer id, String action, int delta) {
if ("like".equals(action)) {
postsRepository.adjustLikeCount(id, delta);
} else {
postsRepository.adjustDislikeCount(id, delta);
}
}
}
@@ -4,7 +4,9 @@ import com.yaoyuan.jiscuss.common.Node;
import com.yaoyuan.jiscuss.common.PostCommonUtil;
import com.yaoyuan.jiscuss.entity.Post;
import com.yaoyuan.jiscuss.entity.custom.PostCustom;
import com.yaoyuan.jiscuss.repository.DiscussionsRepository;
import com.yaoyuan.jiscuss.repository.PostsRepository;
import com.yaoyuan.jiscuss.repository.UsersRepository;
import com.yaoyuan.jiscuss.service.IPostsService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -13,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -23,6 +26,12 @@ public class PostsServiceImpl implements IPostsService {
@Autowired
private PostsRepository postsRepository;
@Autowired
private DiscussionsRepository discussionsRepository;
@Autowired
private UsersRepository usersRepository;
@Transactional(readOnly = true)
@Override
public List<Post> getAllList() {
@@ -48,17 +57,19 @@ public class PostsServiceImpl implements IPostsService {
@Transactional(readOnly = true)
@Override
public List findPostCustomById(Integer id) {
//查询id为1且parentId为null的评论
List<Map<String, Object>> firstposts = postsRepository.findAllByDIdAndparentIdNull(id);
public List findPostCustomById(Integer id, String sort) {
// Choose sort order for top-level posts
List<Map<String, Object>> firstposts = switch (sort == null ? "newest" : sort) {
case "oldest" -> postsRepository.findAllByDIdAndparentIdNullOldest(id);
case "hot" -> postsRepository.findAllByDIdAndparentIdNullHot(id);
default -> postsRepository.findAllByDIdAndparentIdNullNewest(id); // newest (root only, create_time desc)
};
List<PostCustom> firstpostCustomList = PostCommonUtil.getNewPostsObjMap(firstposts);
// List<PostCustom> firstpostCustomListNew = PostCommonUtil.getNewPostsObjCustom(firstpostCustomList);
//查询id为1且parentId不为null的评论
List<Map<String, Object>> thenposts = postsRepository.findAllByDIdAndparentIdNotNull(id);
List<PostCustom> thenpostCustomList = PostCommonUtil.getNewPostsObjMap(thenposts);
// List<PostCustom> thenpostCustomListNew = PostCommonUtil.getNewPostsObjCustom(thenpostCustomList);
//新建一个Node集合。
ArrayList<Node> nodes = new ArrayList<>();
//将第一层评论都添加都Node集合中
@@ -72,15 +83,27 @@ public class PostsServiceImpl implements IPostsService {
//打印回复链表
Node.show(list);
// List<Map<String, Object>> posts = postsRepository.findPostCustomById(id);
// List<PostCustom> postCustomList = PostCommonUtil.getNewPostsObjMap(posts);
// List<PostCustom> postCustomListNew = PostCommonUtil.getNewPostsObjCustom(postCustomList);
return list;
}
@Transactional
@Override
public Post insert(Post post) {
return postsRepository.save(post);
// Auto-assign floor number within the same transaction to ensure consistency
if (post.getDiscussionId() != null) {
int nextNumber = postsRepository.findMaxNumberByDiscussionId(post.getDiscussionId()) + 1;
post.setNumber(nextNumber);
}
Post saved = postsRepository.save(post);
// Maintain discussion comment count and last activity
if (post.getDiscussionId() != null) {
discussionsRepository.incrementCommentCount(post.getDiscussionId(), post.getCreateId(), new Date());
}
// Maintain user comment count
if (post.getCreateId() != null) {
usersRepository.incrementCommentCount(post.getCreateId());
}
return saved;
}
}
@@ -146,6 +146,13 @@ public class UsersServiceImpl implements IUsersService {
return usersRepository.findAllById(ids);
}
@Transactional(readOnly = true)
@Override
public List<User> getTopActiveUsers(int limit) {
return usersRepository.findTop10ActiveUsers(
org.springframework.data.domain.PageRequest.of(0, limit));
}
}
@@ -0,0 +1,58 @@
package com.yaoyuan.jiscuss.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Lightweight User-Agent parser.
* Extracts a human-readable browser name and major version without
* importing a heavy third-party library.
*
* <p>Detection order matters: Edge must be checked before Chrome, and
* Chrome before Safari, because UA strings commonly contain multiple tokens.
*/
public final class UserAgentUtils {
private UserAgentUtils() {}
private static final Pattern EDGE = Pattern.compile("Edg(?:e|A|iOS)?/([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern FIREFOX = Pattern.compile("Firefox/([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern CHROME = Pattern.compile("Chrome/([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern SAFARI = Pattern.compile("Version/([\\d.]+).*Safari", Pattern.CASE_INSENSITIVE);
private static final Pattern OPERA = Pattern.compile("OPR/([\\d.]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern IE = Pattern.compile("(?:MSIE |Trident/.*rv:)([\\d.]+)", Pattern.CASE_INSENSITIVE);
/**
* Returns a short browser description, e.g. "Chrome 120", "Firefox 121", "Safari 17".
* Returns "未知浏览器" when detection fails.
*
* @param userAgent the raw User-Agent header value
* @return display string, never null
*/
public static String parseBrowser(String userAgent) {
if (userAgent == null || userAgent.isBlank()) {
return "未知浏览器";
}
// Order matters: Edge contains 'Chrome', Chrome contains 'Safari'
String r;
if ((r = match(EDGE, userAgent, "Edge")) != null) return r;
if ((r = match(OPERA, userAgent, "Opera")) != null) return r;
if ((r = match(FIREFOX, userAgent, "Firefox")) != null) return r;
if ((r = match(CHROME, userAgent, "Chrome")) != null) return r;
if ((r = match(SAFARI, userAgent, "Safari")) != null) return r;
if ((r = match(IE, userAgent, "IE")) != null) return r;
return "未知浏览器";
}
private static String match(Pattern pattern, String ua, String name) {
Matcher m = pattern.matcher(ua);
if (m.find()) {
String version = m.group(1);
// Only keep major version number
int dotIdx = version.indexOf('.');
String major = dotIdx > 0 ? version.substring(0, dotIdx) : version;
return name + " " + major;
}
return null;
}
}