1.RedisKeyUtil:
public class RedisKeyUtil {
private static final String SPLIT=":";
private static final String PREFIX_ENTITY_LIKE="like:entity";
private static final String PREFIX_USER_LIKE="like:user";
private static final String PREFIX_FOLLOWEE="followee";
private static final String PREFIX_FOLLOWER="follower";
private static final String PREFIX_KAPTCHA="kaptcha";
private static final String PREFIX_TICKET="ticket";
private static final String PREFIX_USER="user";
//某个实体的赞
//like:entity:entityType:entityId->set(userId)
public static String getEntityLikeKey(int entityType,int entityId){
return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;
}
//某个用户的赞
//like:user:userId->int
public static String getUserLikeKey(int userId){
return PREFIX_USER_LIKE+SPLIT+userId;
}
//某个用户关注的实体
//followee:userId:entityType->zset(entityId,now)
public static String getFolloweeKey(int userId,int entityType){
return PREFIX_FOLLOWEE+SPLIT+userId+SPLIT+entityType;
}
//某个实体拥有的粉丝
//follower:entityType:entityId->zset(userId,now)
public static String getFollowerKey(int entityType,int entityId){
return PREFIX_FOLLOWER+SPLIT+entityType+SPLIT+entityId;
}
//登录验证码
public static String getKaptchaKey(String owner){
return PREFIX_KAPTCHA+SPLIT+owner;
}
//登录凭证
public static String getTicketKey(String ticket){
return PREFIX_TICKET+SPLIT+ticket;
}
//用户
public static String getUserKey(int userId){
return PREFIX_USER+SPLIT+userId;
}
}
2.FollowService:
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.RedisKeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class FollowService implements CommunityConstant {
@Autowired
@Qualifier("redisTemplates")
private RedisTemplate redisTemplate;
@Autowired
private UserServiceImpl userService;
//关注
public void follow(int userId,int entityType,int entityId){
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
redisOperations.multi();
redisOperations.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis());
redisOperations.opsForZSet().add(followerKey,userId,System.currentTimeMillis());
return redisOperations.exec();
}
});
}
//取消关注
public void unfollow(int userId,int entityType,int entityId){
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
redisOperations.multi();
redisOperations.opsForZSet().remove(followeeKey,entityId);
redisOperations.opsForZSet().remove(followerKey,userId);
return redisOperations.exec();
}
});
}
//查询某一用户关注的实体的数量
public long selectFolloweeCount(int userId,int entityType){
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().zCard(followeeKey);
}
//查询某一实体的粉丝数量
public long selectFollowerCount(int entityType,int entityId){
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followerKey);
}
//判断当前用户是否已经关注该实体
public boolean hasFollowed(int userId,int entityType,int entityId){
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().score(followerKey,userId)!=null;
}
//查询某用户关注的人
public List<Map<String,Object>> selectFollowees(int userId,int offset,int limit){
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
if (targetIds==null){
return null;
}
List<Map<String,Object>> list=new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String,Object> map=new HashMap<>();
User user = userService.selectById(targetId);
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
//查询某用户的粉丝
public List<Map<String,Object>> selectFollowers(int userId,int offset,int limit){
String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset+limit-1);
if (targetIds==null){
return null;
}
List<Map<String,Object>> list=new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String,Object> map=new HashMap<>();
User user = userService.selectById(targetId);
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
map.put("followTime",new Date(score.longValue()));
list.add(map);
}
return list;
}
}
3.FollowController:
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.FollowService;
import com.nowcoder.community.service.UserServiceImpl;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
@Controller
public class FollowController {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserServiceImpl userService;
@PostMapping("/follow")
@ResponseBody
public String follow(int entityType,int entityId){
User user = hostHolder.getUser();
followService.follow(user.getId(), entityType,entityId);
return CommunityUtil.getJSONString(0,"已关注");
}
@PostMapping("/unfollow")
@ResponseBody
public String unfollow(int entityType,int entityId){
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityType,entityId);
return CommunityUtil.getJSONString(0,"已取消关注");
}
private boolean hasFollowed(int userId){
if (hostHolder.getUser()==null){
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), CommunityConstant.ENTITY_TYPE_USER,userId);
}
@GetMapping("/followees/{userId}")
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model){
User user = userService.selectById(userId);
if (user==null){
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user",user);
page.setPath("/followees/"+userId);
page.setLimit(5);
page.setRows((int) followService.selectFolloweeCount(userId,CommunityConstant.ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.selectFollowees(userId, page.getOffset(), page.getLimit());
if (userList!=null){
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed",hasFollowed(u.getId()));
}
}
model.addAttribute("users",userList);
return "/site/followee";
}
@GetMapping("/followers/{userId}")
public String getFollowers(@PathVariable("userId") int userId,Page page,Model model){
User user = userService.selectById(userId);
if (user==null){
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user",user);
page.setLimit(5);
page.setPath("/followers/"+userId);
page.setRows((int) followService.selectFollowerCount(CommunityConstant.ENTITY_TYPE_USER,userId));
List<Map<String, Object>> userList = followService.selectFollowers(userId, page.getOffset(), page.getLimit());
if (userList!=null){
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed",hasFollowed(u.getId()));
}
}
model.addAttribute("users",userList);
return "/site/follower";
}
}
4.profile.html:
<!-- 个人信息 -->
<div class="media mt-5">
<img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle" alt="用户头像" style="width:50px;">
<div class="media-body">
<h5 class="mt-0 text-warning">
<span th:utext="${user.username}">nowcoder</span>
<input type="hidden" id="entityId" th:value="${user.id}">
<button type="button" th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|" th:text="${hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null&&loginUser.id!=user.id}">关注TA</button>
</h5>
<div class="text-muted mt-3">
<span>注册于 <i class="text-muted" th:text="${#dates.format(user.createTime,'yyyy-MM-dd HH:mm:ss')}">2015-06-12 15:20:12</i></span>
</div>
<div class="text-muted mt-3 mb-5">
<span>关注了 <a class="text-primary" th:href="@{|/followees/${user.id}|}" th:text="${followeeCount}">5</a> 人</span>
<span class="ml-4">关注者 <a class="text-primary" th:href="@{|/followers/${user.id}|}" th:text="${followerCount}">123</a> 人</span>
<span class="ml-4">获得了 <i class="text-danger" th:text="${likeCount}">87</i> 个赞</span>
</div>
</div>
5.profile.js:
$(function(){
$(".follow-btn").click(follow);
});
function follow() {
var btn = this;
if($(btn).hasClass("btn-info")) {
// 关注TA
$.post(
CONTEXT_PATH + "/follow",
{"entityType":3,"entityId":$(btn).prev().val()},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
window.location.reload();
} else {
alert(data.msg);
}
}
);
// $(btn).text("已关注").removeClass("btn-info").addClass("btn-secondary");
} else {
// 取消关注
$.post(
CONTEXT_PATH + "/unfollow",
{"entityType":3,"entityId":$(btn).prev().val()},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
window.location.reload();
} else {
alert(data.msg);
}
}
);
//$(btn).text("关注TA").removeClass("btn-secondary").addClass("btn-info");
}
}
6.UserController:
//个人主页
@GetMapping("/profile/{userId}")
public String getProfilePage(@PathVariable("userId") int userId,Model model){
User user = userService.selectById(userId);
if (user==null){
throw new RuntimeException("该用户不存在!");
}
//用户
model.addAttribute("user",user);
//点赞数量
int likeCount = likeService.selectUserLikeCount(userId);
model.addAttribute("likeCount",likeCount);
//关注数量
long followeeCount = followService.selectFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount",followeeCount);
//粉丝数量
long followerCount = followService.selectFollowerCount(ENTITY_TYPE_USER, userId);
model.addAttribute("followerCount",followerCount);
//当前登录用户是否已关注
boolean hasFollowed=false;
if(hostHolder.getUser()!=null){
hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed",hasFollowed);
return "/site/profile";
}
7.followee.html:
<!-- 内容 -->
<div class="main">
<div class="container">
<div class="position-relative">
<!-- 选项 -->
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link position-relative active" th:href="@{|/followees/${user.id}|}"><i class="text-info" th:utext="${user.username}">Nowcoder</i> 关注的人</a>
</li>
<li class="nav-item">
<a class="nav-link position-relative" th:href="@{|/followers/${user.id}|}">关注 <i class="text-info" th:utext="${user.username}">Nowcoder</i> 的人</a>
</li>
</ul>
<a th:href="@{|/profile/${user.id}|}" class="text-muted position-absolute rt-0">返回个人主页></a>
</div>
<!-- 关注列表 -->
<ul class="list-unstyled">
<li class="media pb-3 pt-3 mb-3 border-bottom position-relative"th:each="map:${users}">
<a th:href="@{|/profile/${map.user.id}|}">
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="media-body">
<h6 class="mt-0 mb-3">
<span class="text-success" th:text="${map.user.username}">落基山脉下的闲人</span>
<span class="float-right text-muted font-size-12">关注于 <i th:text="${#dates.format(map.followTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</i></span>
</h6>
<div>
<input type="hidden" id="entityId" th:value="${map.user.id}">
<button type="button" th:class="|btn ${map.hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|" th:text="${map.hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null&&loginUser.id!=map.user.id}">关注TA</button>
</div>
</div>
</li>
</ul>
<!-- 分页 -->
<nav class="mt-5" th:replace="~{index::pagination}">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="#">首页</a></li>
<li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">下一页</a></li>
<li class="page-item"><a class="page-link" href="#">末页</a></li>
</ul>
</nav>
</div>
</div>
8.follower.html:
<!-- 内容 -->
<div class="main">
<div class="container">
<div class="position-relative">
<!-- 选项 -->
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link position-relative" th:href="@{|/followees/${user.id}|}"><i class="text-info" th:utext="user.username">Nowcoder</i> 关注的人</a>
</li>
<li class="nav-item">
<a class="nav-link position-relative active" th:href="@{|/followers/${user.id}|}">关注 <i class="text-info" th:utext="user.username">Nowcoder</i> 的人</a>
</li>
</ul>
<a th:href="@{|/profile/${user.id}|}" class="text-muted position-absolute rt-0">返回个人主页></a>
</div>
<!-- 粉丝列表 -->
<ul class="list-unstyled">
<li class="media pb-3 pt-3 mb-3 border-bottom position-relative"th:each="map:${users}">
<a th:href="@{|/profile/${map.user.id}|}">
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="media-body">
<h6 class="mt-0 mb-3">
<span class="text-success" th:utext="${map.user.username}">落基山脉下的闲人</span>
<span class="float-right text-muted font-size-12">关注于 <i th:text="${#dates.format(map.followTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</i></span>
</h6>
<div>
<input type="hidden" id="entityId" th:value="${map.user.id}">
<button type="button" th:class="|btn ${map.hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|" th:text="${map.hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null&&loginUser.id!=map.user.id}">关注TA</button>
</div>
</div>
</li>
</ul>
<!-- 分页 -->
<nav class="mt-5" th:replace="~{index::pagination}">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="#">首页</a></li>
<li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">下一页</a></li>
<li class="page-item"><a class="page-link" href="#">末页</a></li>
</ul>
</nav>
</div>
</div>