仿牛客网项目(七)
点赞功能
点赞
功能分析
可以对帖子、评论点赞;
第一次点赞,第二次取消点赞;
统计帖子或评论的点赞数量;
前端显示帖子或评论点赞数量和点赞状态;
功能实现
为了方便获取RedisKey,在util包下新建 RedisKeyUtil工具类,代码如下:
public class RedisKeyUtil {
public static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
}
service层,新建LikeService类,代码如下:
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞
public void like(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
Boolean ismember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if (ismember) {
redisTemplate.opsForSet().remove(entityLikeKey, userId);
} else {
redisTemplate.opsForSet().add(entityLikeKey, userId);
}
}
// 查询某实体点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
// 查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}
}
在DiscussPostController修改getDiscussPost方法,添加查看点赞数量和点赞状态,代码如下:
@Autowired
private LikeService likeService;
/**
* 查看帖子详情
* @param discussPostId
* @param model
* @return
*/
@GetMapping( "/detail/{discussPostId}")
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 查询帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 查找作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
// 点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,discussPostId);
model.addAttribute("likeCount",likeCount);
// 点赞状态
int likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(),ENTITY_TYPE_POST,discussPostId);
model.addAttribute("likeStatus",likeStatus);
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());
// 评论: 给帖子的评论
// 回复: 给评论的评论
// 评论列表
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 评论VO列表
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO
Map<String, Object> commentVo = new HashMap<>();
// 评论
commentVo.put("comment", comment);
// 查询到评论的作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT,comment.getId());
commentVo.put("likeCount",likeCount);
// 点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(),ENTITY_TYPE_COMMENT,comment.getId());
commentVo.put("likeStatus",likeStatus);
// 回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复VO列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
//点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT,reply.getId());
replyVo.put("likeCount",likeCount);
//点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(),ENTITY_TYPE_COMMENT,reply.getId());
replyVo.put("likeStatus",likeStatus);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
在HomeController中修改getIndexPages方法,添加查看点赞数量,代码如下:
/**
* 获取用户名和对应的评论
* @param model 前端返回模型
* @return
*/
@GetMapping("/index")
public String getIndexPages(Model model, Page page){
/**方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model。
* 所以,在thymeleaf中可以直接访问Page对象中的数据*/
/**查询总行数*/
page.setRows(discussPostService.findDiscussPostRows(0));
/**复用页面路径*/
page.setPath("/index");
/**查询所有的评论*/
List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
/**创建用于存储用户对象和对应评论map*/
List<Map<String, Object>> discussPosts = new ArrayList<>();
/**判断查询是否为空*/
if(list != null) {
/**遍历查询到的评论对象*/
for (DiscussPost post : list) {
Map<String, Object> map = new HashMap<>();
/**根据用户id查询用户对象*/
User user = userService.findUserById(post.getUserId());
map.put("post",post);
map.put("user",user);
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,post.getId());
map.put("likeCount",likeCount);
/**封存用户和对应的评论*/
discussPosts.add(map);
}
}
/**返回前端对象*/
model.addAttribute("discussPosts",discussPosts);
return "/index";
}
前端修改discuss.js、index.html文件,详情见项目源代码;
功能测试
启动项目,点赞帖子和评论,查看点赞状态是否发生变化;


查看点赞
功能分析
查看个人主页,统计点赞数量;
功能实现
util包下,RedisKeyUtil新增getUserLikeKey,代码如下:
public class RedisKeyUtil {
public static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
private static final String PREFIX_USER_LIKE = "like: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;
}
}
service层,对like方法进行重构,新增findUserLikeCount方法,代码如下:
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞
public void like(int userId, int entityType, int entityId, int entityUserId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
operations.multi();
if (isMember) {
operations.opsForSet().remove(entityLikeKey, userId);
operations.opsForValue().decrement(userLikeKey);
} else {
operations.opsForSet().add(entityLikeKey, userId);
operations.opsForValue().increment(userLikeKey);
}
return operations.exec();
}
});
}
// 查询某个用户获得的赞
public int findUserLikeCount(int userId) {
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}
}
controller层对like方法进行重构,代码如下:
/**
* 对帖子或评论点赞
* @param entityType
* @param entityId
* @return
*/
@PostMapping("/like")
@ResponseBody
public String like(int entityType, int entityId,int entityUserId) {
User user = hostHolder.getUser();
// 点赞zzz
likeService.like(user.getId(), entityType, entityId,entityUserId);
// 数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
return CommunityUtil.getJSONString(0, null, map);
}
UserController中添加用户主页getProfilePage方法,代码如下:
/**
* 用户主页
* @param userId
* @param model
* @return
*/
@GetMapping("/profile/{userId}")
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
// 用户
model.addAttribute("user", user);
// 点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);
return "/site/profile";
}
前端修改discuss-detail.js、disscuss-detail等文件,详情见源代码;
功能测试
启动项目,点赞用户,查看用户主页点赞信息;

关注功能
关注、取消关注
功能分析
关注的目标可以是用户、帖子、题目,将这些目标抽象为实体;
关注者也是粉丝;
统计关注、被关注的个数;
功能实现
util包下,在RedisKeyUtil工具类中,新增getFolloweeKey和getFollowerKey方法,代码如下:
/**
* @author ahtoh
* @version 1.0
* @date 2023/3/31 10:27
*/
public class RedisKeyUtil {
public 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";
// 某个实体的赞
// 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;
}
// 某个用户关注的实体(键:用户Id 值:实体Id)
// followee:userId:entityType -> zset(entityId, now)
public static String getFolloweeKey(int userId, int entityType) {
return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}
// 某个实体拥有的粉丝 (键:实体Id 值:用户Id)
// follower:entityType:entityId -> zset(userId, now)
public static String getFollowerKey(int entityType, int entityId) {
return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}
}
service层,新建FollowService方法,实现关注实体、取消关注实体。代码如下:
@Service
public class FollowService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 关注实体
* @param userId 用户id
* @param entityType 实体类型
* @param entityId 实体id
*/
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
return operations.exec();
}
});
}
/**
* 取消关注
* @param userId
* @param entityType
* @param entityId
*/
public void unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().remove(followeeKey, entityId);
operations.opsForZSet().remove(followerKey, userId);
return operations.exec();
}
});
}
// 查询关注的实体的数量
public long findFolloweeCount(int userId, int entityType) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().zCard(followeeKey);
}
// 查询实体的粉丝数量
public long findFollowerCount(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 followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
}
controller层,新建FollowController,代码如下:
@Controller
public class FollowController {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
/**
* 关注实体
* @param entityType
* @param entityId
* @return
*/
@PostMapping("/follow")
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.follow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已关注!");
}
/**
* 取消关注
* @param entityType
* @param entityId
* @return
*/
@PostMapping("/unfollow")
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已取消关注!");
}
}
UserController中,重构getProfillePage方法,添加关注数量、粉丝数量和是否关注的方法,代码如下:
/**
* 用户主页
* @param userId
* @param model
* @return
*/
@GetMapping("/profile/{userId}")
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
// 用户
model.addAttribute("user", user);
// 点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);
// 关注数量
long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount", followeeCount);
// 粉丝数量
long followerCount = followService.findFollowerCount(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";
}
前端修改profile.index,详情见源代码;
功能测试
启动项目,登录用户,关注用户,查看关注;

查看关注、查看粉丝
功能分析
查询某个用户关注的人,支持分页;
查询某个用户的粉丝,支持分页;
功能实现
service层,FollowService中新增findFollowees、findFollowers方法,代码如下:
// 查询某用户关注的人
public List<Map<String, Object>> findFollowees(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.findUserById(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>> findFollowers(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.findUserById(targetId);
map.put("user", user);
Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
map.put("followTime", new Date(score.longValue()));
list.add(map);
}
return list;
}
controller层,FollowController中新增getFollowees、getFollowers、hasFollowed方法,代码如下:
/**
* 查看关注
* @param userId
* @param page
* @param model
* @return
*/
@GetMapping("/followees/{userId}")
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.findFollowees(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";
}
/**
* 查看粉丝
* @param userId
* @param page
* @param model
* @return
*/
@GetMapping("/followers/{userId}")
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followers/" + userId);
page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));
List<Map<String, Object>> userList = followService.findFollowers(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";
}
/**
* 判断是否又被关注
* @param userId
* @return
*/
private boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
前端修改profile.html、followee.html、follower.html,详情见源代码;
功能测试
启动项目,登录用户,查看关注者和粉丝;


优化登录
功能分析
优化登录,主要考虑利用Redis特性,实现对频繁访问的数据的存储,以提高系统性能;用户每次登录过程中,频繁请求的数据包括验证码、每次请求过程携带的用户登录凭证信息、用户信息等;
- Redis 存储验证码
- 验证码需要频繁的访问与刷新,对性能要求较高。
- 验证码不需永久保存,通常在很短的时间后就会失
- 分布式部署时,存在Session共享的问题。
- Redis存储登录凭证
- 处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。
- Redis缓存用户信息
- 处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高。
功能实现
首先,获取验证码、登录凭证、用户信息的RedisKey,在RedisKeyUtil工具类中新增如下代码:
private static final String PREFIX_KAPTCHA = "kaptcha";
private static final String PREFIX_TICKET = "ticket";
private static final String PREFIX_USER = "user";
// 登录验证码
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;
}
dao层,在 LoginTicketMapper 类中添加 Deprecated 注解;
在 UserSddervice 中修改 findUserId、login、findLoginTicket、updateheader方法,并且添加 getCache、initCache、clearCache 方法,代码如下:
@Service
public class UserService implements CommunityConstant {
@Autowired
private UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
// @Autowired
// private LoginTicketMapper loginTicketMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据用户id查询用户
* @param id
* @return
*/
public User findUserById(int id) {
// return userMapper.selectById(id);
User user = getCache(id);
if(user == null){
initCache(id);
}
return user;
}
/**
* 用户注册
* @param user
* @return
*/
public Map<String, Object> register(User user) {
Map<String, Object> map = new HashMap<>();
//用户、账号、密码、邮箱判空
if (user == null) {
throw new IllegalArgumentException("参数不能为空!");
}
if (StringUtils.isBlank(user.getUsername())) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(user.getPassword())) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
if (StringUtils.isBlank(user.getEmail())) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
// 验证邮箱合法性
boolean email = CommunityUtil.isEmail(user.getEmail());
if(!email){
map.put("emailMsg","邮箱不正确!");
return map;
}
// 验证账号是否已注册
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg", "该账号已存在!");
return map;
}
// 验证邮箱是否已注册
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg", "该邮箱已被注册!");
return map;
}
// 注册用户
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.MD5(user.getPassword() + user.getSalt()));
user.setType(0);
user.setStatus(0);
user.setActivationCode(CommunityUtil.generateUUID());
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
user.setCreateTime(new Date());
userMapper.insertUser(user);
return map;
}
/**
* 向指定用户发送邮件
* @param username 用户名
*/
public void sendMail(String username){
User user = userMapper.selectByName(username);
/**设置邮件模版*/
Context context = new Context();
context.setVariable("email",user.getEmail());
/**设置处理请求的路径*/
//http://localhost:8086/community/activation/userId/code
String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
context.setVariable("url",url);
String process = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(),"激活账号",process);
}
/**
* 用户激活邮件
* @param userId
* @param code
* @return
*/
public int activation(int userId, String code) {
User user = userMapper.selectById(userId);
if (user.getStatus() == 1) {
return ACTIVATION_REPEAT;
} else if (user.getActivationCode().equals(code)) {
userMapper.updateStatus(userId, 1);
return ACTIVATION_SUCCESS;
} else {
return ACTIVATION_FAILURE;
}
}
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @param expiredSeconds 有效时间
* @return
*/
public Map<String, Object> login(String username, String password, int expiredSeconds) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (StringUtils.isBlank(username)) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
// 验证账号
User user = userMapper.selectByName(username);
if (user == null) {
map.put("usernameMsg", "该账号不存在!");
return map;
}
// 验证状态
if (user.getStatus() == 0) {
map.put("usernameMsg", "该账号未激活!");
return map;
}
// 验证密码
password = CommunityUtil.MD5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
map.put("passwordMsg", "密码不正确!");
return map;
}
// 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// loginTicketMapper.insertLoginTicket(loginTicket);
String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(redisKey,loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;
}
/**
* 退出登录
* @param ticket
*/
public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
redisTemplate.opsForValue().set(redisKey,loginTicket);
}
/**
* 查找用户登录凭证信息
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
/**
* 修改头像信息
* @param id
* @param headerUrl
*/
public int updateHeader(int id, String headerUrl) {
// userMapper.updateHeader(id,headerUrl);
int rows = userMapper.updateHeader(id,headerUrl);
clearCache(id);
return rows;
}
/**
* 根据姓名查询用户
* @param username
* @return
*/
public User findUserByName(String username){
return userMapper.selectByName(username);
}
// 1. 优先从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2. 取不到时初始化缓存数据
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
// 3. 数据变更时清除缓存数据
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
}
controller层,在 LoginController 类中修改 getKaptcha、login 方法,代码如下:
@Controller
public class LoginController implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
private UserService userService;
@Autowired
private Producer kaptchaProducer;
@Value("${server.servlet.context-path}")
private String contextPath;
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取注册页面
* @return
*/
@GetMapping("/register")
public String getRegisterPage() {
return "/site/register";
}
/**
* 获取登录页面
* @return
*/
@GetMapping("/login")
public String getLoginPage() {
return "/site/login";
}
/**
* 用户注册
* @param model
* @param user
* @return
* @throws IllegalAccessException
*/
@PostMapping("/register")
public String register(Model model, User user) throws IllegalAccessException {
Map<String, Object> map = userService.register(user);
/**map为空或者为null,无报错信息*/
if (map == null || map.isEmpty()) {
userService.sendMail(user.getUsername());
model.addAttribute("msg", "注册成功,我们已经向您的邮箱发送了一份激活邮件,请尽快激活!");
model.addAttribute("target", "/index");
return "/site/operate-result";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
model.addAttribute("emailMsg", map.get("emailMsg"));
return "/site/register";
}
}
/**
* 激活账号
* @param model
* @param userId
* @param code
* @return
*/
// http://localhost:8080/community/activation/101/code
@GetMapping("/activation/{userId}/{code}")
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
int result = userService.activation(userId, code);
if (result == ACTIVATION_SUCCESS) {
model.addAttribute("msg", "激活成功,您的账号已经可以正常使用了!");
model.addAttribute("target", "/login");
} else if (result == ACTIVATION_REPEAT) {
model.addAttribute("msg", "无效操作,该账号已经注册过了!");
model.addAttribute("target", "/index");
} else {
model.addAttribute("msg", "激活失败,您提供的激活码不正确!");
model.addAttribute("target", "/index");
}
return "/site/operate-result";
}
/**
* 获取验证码
* @param response
* @param
*/
@GetMapping("/kaptcha")
public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 将验证码存入session
// session.setAttribute("kaptcha", text);
//验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
//将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
// 将图片输出给浏览器
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param rememberme 是否记住用户
* @param model 前端模型
* @param
* @param response 响应
* @return
*/
@PostMapping("/login")
public String login(String username, String password, String code, boolean rememberme,
Model model, /*HttpSession session, */HttpServletResponse response,@CookieValue("kaptchaOwner") String kaptchaOwner) {
// 从服务端session中获取验证码,判断验证码是否正确
// String kaptcha = (String) session.getAttribute("kaptcha");
String kaptcha = null;
if (StringUtils.isNotBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "/site/login";
}
// 根据登录信息查找登录凭证
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
//在此路径上生效
cookie.setPath(contextPath);
//cookie生效时间
cookie.setMaxAge(expiredSeconds);
//将cookie响应给前端
response.addCookie(cookie);
return "redirect:/index";
} else {
//模型中加入错误信息,用于thymeleaf动态显示
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
/**
* 用户退出登录
* @param ticket 登录凭证信息
* @return
*/
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
}