【项目笔记】
仿牛客网社区项目,主页显示大家发布的贴子。我发现难的是前端怎么写。。。。
图片说明
先进行数据库设计:
用户user表:

  • id(自增)/userId/username/password/email/head_url/create_time/type(普通用户、版主、管理员)
    讨论贴discuss_post表:
  • id/userId/title/content/create_time/comment_count(回帖数,后期考虑加入)
    main/java/com.nowcoder包下:
  • entity(实体类):User、DiscussPost
  • controller(控制层):HomeController(/index)
  • service(业务逻辑层):DiscussPostService、UserService
  • dao(持久层):UserMapper、DiscussPostMapper

【首页】
反向思考,首页要查帖子,还要显示是谁发的帖子
DiscussPostMapper:查帖子函数selectDiscussPosts,应该返回一个List<discusspost>,sql语句在rescources文件夹下开发
UserMapper:根据帖子id查用户信息,返回user对象</discusspost>

Service层:充当了工具人角色

HomeController:得到List<discusspost>,定义List<Map<String, Object>>,每个map存user和post</discusspost>

rescources文件夹下:
discusspost-mapper.xml
select * from discuss_post order by create_time desc
limit #{offset}, #{limit}(考虑到评论分页,传入起始,每页条数)
(不要写*,最好写所有字段)
user-mapper.xml

【注册】
图片说明

LoginController接受表单参数,调用UserService的register方法,验证账户合理性,然后调用userMapper.insertUser(user);

【登录】
图片说明
登录这块涉及到登录状态的问题,使用cookie来进行会话管理。需要一个表来记录账户的状态、token、是否过期等。
login_ticket表:id/userid/ticket/status/expired_time
经过LoginController的login方法需要传入HttpServletResponse设置cookie,调用userService.login验证登录,后设置cookie,重定向到首页。

登录前后显示的界面是不一样的,那么怎么根据用户cookie携带的状态来显示不同的信息?这就用到了拦截器了。

过滤器和拦截器的区别:

  ①拦截器是基于java的反射机制的,而过滤器是基于函数回调。
  ②拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
  ③拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
  ④拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
  ⑤在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
  ⑥拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

定义LoginTicketInterceptor实现Interceptor,它有三个方法preHandle在controller之前调用,在这个方法里我们根据cookie得到用户状态,并且查到这次用户实体类user,存入ThreadLocal(解决并发问题)。我们经常用到这个用户信息,所有存下来用。postHandle函数里可以把user传给前端。
定义Interceptor后还要进行注册,定义WebMvcConfig实现WebMvcConfigurer,进行注册即可。

【发帖/帖子详情】
也是增删改查,注意显示帖子详情controller方法中需要传入discussPostId。
图片说明

【评论显示/添加】
comment表:
id/userid(评论发布者)/entity_type(评论类型,对帖子的评论还是对评论的评论)/
entity_id(比如id为228的帖子)/target_id(专门指向人)/content/status(正常还是删除了)/create_time
同样增删改查,更为复杂,添加评论用到了事务。声明式和编程式两种
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
在业务层使用。

【私信列表和发送列表】
message表:
图片说明
conversation_id用于标识是不是一次会话
图片说明
点开后:
图片说明
这块仍然是增删改查数据库,没有特别值得记录的地方。
【统一异常和日志处理】
异常:
类注解@ControllerAdvice(annotations = Controller.class)
怎么处理的方法 @ExceptionHandler({ Exception.class })
通知(AOP):
类注解:@Component @Aspect
切入点:@Pointcut("execution(* com.nowcoder.community.service..(..))")
处理函数:@Before("pointcut()")

【Redis】
哪些用到Redis:点赞/收到的赞/关注/取消关注/关注列表/粉丝列表/登录token/findUserById

【点赞/关注】
Redis的key和类型设计:
某个实体的赞
like:entity:entityType:entityId -> set(userId)
某个用户的赞
like:user:userId -> int
某个用户关注的实体
followee:userId:entityType -> zset(entityId,now)
某个实体拥有的粉丝
follower:entityType:entityId -> zset(userId,now)

    // 点赞
    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();
            }
        });
    }

    @RequestMapping(path = "/like", method = RequestMethod.POST)
    @ResponseBody
    public String like(int entityType, int entityId, int entityUserId) {
        User user = hostHolder.getUser();

        // 点赞
        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);
    }

【登录优化】

   // loginTicketMapper.insertLoginTicket(loginTicket);
        String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
        redisTemplate.opsForValue().set(redisKey, loginTicket);

 // 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);
    }