牛客网中级项目笔记

目录

1. 项目实现功能

2. 项目整体架构

2.1 架构及内容

2.2 功能:

2.3 业务逻辑

2.3.1 注册、登录、登出

2.3.2 拦截器

2.3.3 图片上传

2.3.4 发布news和comment

2.3.5 Redis实现点赞点踩

2.3.6 异步处理


1. 项目实现功能

  • 用户注册、登录
  • 资讯发布、资讯排序、咨询分类
  • 图片上传及管理
  • 资讯首页分页展示
  • 用户评论、用户点赞、用户点踩
  • 邮件通知
  • 站内信通知

2. 项目整体架构

2.1 架构及内容

  • 项目是B/S(Browser/Server,浏览器/服务器架构),使用SpringBoot牛客网中级项目笔记_第1张图片
  • 数据库:Mysql
  • 持久化框架:Mybatis
  • 缓存:Redis
  • 模版: velocity
  • 项目构建:maven

 

  1. Controller:解析web请求,调用相应的Service方法处理,并返回ModelAndView或者Json格式结果;
  2. Service:业务层,包含相应的业务逻辑,调用DAO层相应的方法来操纵数据库;
  3. DAO(data access object):数据处理层,通过Mybatis框架,处理Mysql中相应的数据操作;
  4. model:项目中的实体类:News、User、Message、Comment、LoginTicket(起token的作用,注册登录的用户会发一个ticket,ticket过期/失效后,重新登录后,会重新发送一个ticket)、HostHolder(登录用户且ticket有效时,绑定一个hostHolder,将该请求与对应的user绑定,当该请求响应返回ModelAndView时,将user的信息添加到ModelAndView中)
  5. util:工具类。JedisAdapter:封装了redis一些操作(list、set相关的操作);RedisKeyUtil:返回redis中对应的key(如:like的key、dislike的key);MailSender:封装邮件相关的操作;ToutiaoUtil:封装了一些基本的操作(如:json的操作、文件加密等操作)
  6. interceptor:拦截器,拦截所有请求,判断用户登录状态,如果已登录且ticket未失效,则绑定hostholder;拦截用户消息列表页、详情页的请求(/msg/*),如果未登录,则重定向到提示登录的页面;
  7. configuration:配置类,实现了WebMvcConfigurer接口,添加拦截器;
  8. async:异步操作的包;

 

2.2 功能:

  1. 登录、注册、登出;
  2. 资讯首页:
    • 每页展示20条资讯,分页展示;
    • 每条资讯显示数据:like数量(用户赞的状态)、图片、标题、链接、评论数、发布者信息(头像、姓名)
    • 牛客网中级项目笔记_第2张图片

       

  3. 资讯详情页:
    • 资讯的详情数据:like数量(用户赞的状态)、图片、标题、链接、评论数、发布者信息(头像、姓名)、评论详情;
    • 可以评论该资讯;
    • 牛客网中级项目笔记_第3张图片

       

  4. 站内信列表页
    • 用户发布的资讯收到的相关信息(赞、踩、评论)
    • 显示未读信息的数量
    • 牛客网中级项目笔记_第4张图片

       

  5. 站内信详情页
    • 显示具体的站内信
    • 牛客网中级项目笔记_第5张图片

       

数据库中表:

  1. user
  2. news
  3. comment
  4. message
  5. login_ticket

 

2.3 业务逻辑

2.3.1 注册、登录、登出

统一的数据格式:{code:0,msg:”,data:”} :使用fastJson返回Json格式的结果{code:"xx",message:"xxxxxx"}; controller方法使用@ResponseBody注解

注册用户;传参:username、password、remberme

  1. 用户名合法性检测(长度,敏感词,重复,特殊字符)
  2. 密码长度要求
  3. 是否已注册
  4. 随机生成salt,使用MD5对password+salt进行加密;添加user到数据库中
  5. 添加LoginTicket
  6. 将ticket添加到response的cookie中;如果设置了remberme,则设定一个cookie过期时间;不设定cookie过期时间,则当前窗口关闭,该cookie就会过期。

 

用户登录

  1. 用户名是否存在、是否为空;
  2. 密码是否正确、是否为空;
  3. 正确的用户名密码,则添加LoginTicket;
  4. 将ticket添加到response的cookie中;如果设置了remberme,则设定一个cookie过期时间;不设定cookie过期时间,则当前窗口关闭,该cookie就会过期。

用户登出/logout/

设置ticket状态为1(失效);

 

2.3.2 拦截器

PassportInterceptor拦截器,它对于所有页面都进行处理

1. 可以知道用户是哪个用户。 

注册成功后会进行自动登陆,对于登陆,在登陆操作中,在service层,进行逻辑判断,对上返回状态回到controller,对下dao去和数据库交互。在service登陆代码中,服务器会生成一个string类型的ticket,存入cookie中,key值是ticket,value值是ticket的值。通过response下发到浏览器。 
下次在已经登陆的用户,进行其他点击后。在进入controller前,调用preHandle方法处理,它可以检查客户端提交的cookie中是否有服务器之前下发的ticket,如果有证明这个请求是已经登陆的用户了。把登陆的用户放到线程本地变量。在此时才进入controller,可以拿到具体的用户HostHolder类,这是线程本地变量,可以根据登陆的用户进行个性化渲染,比如关注用户的动态,个人收藏等。

二、可以进行权限管理 

某些页面需要用户已登录(比如:站内信页面)或者用户是某个等级的。可以现在preHandler中判断,这个可以在设置一个特定范围的拦截器,如下的拦截器LoginRequiredInterceptor,在访问setting页面时才会进入到该拦截器,如果验证没有HostHolder,说明用户没登陆(没有hostholder说明该用户未登录,或ticket已过期/已失效),就跳转到主页或者给出登陆页面。拦截器也先后执行顺序。在配置类中ToutiaoWebConfiguration中先后决定。

三、可以自动登陆 

用户请求中cookie包含的sessionId(就是ticket),服务端的拦截器判断该sessionId是否和服务器的ticket关联起来,如果关联,且ticket未过期、未失效时,那么就会知道该请求对应的是哪一个用户,可以在ModelAndView中添加相应的user,可以做到自动登录的功能。
 

2.3.3 图片上传

MultipartFile接口

一、post方法上传图片到服务器本地,返回访问图片的url

 public String saveImage(MultipartFile file) throws IOException {
        int dotPos = file.getOriginalFilename().lastIndexOf(".");
        if (dotPos < 0) {
            return null;
        }
        String fileExt = file.getOriginalFilename().substring(dotPos + 1).toLowerCase();
        if (!ToutiaoUtil.isFileAllowed(fileExt)) {
            return null;
        }

        String fileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileExt;
        Files.copy(file.getInputStream(), new File(ToutiaoUtil.IMAGE_DIR + fileName).toPath(),
                StandardCopyOption.REPLACE_EXISTING);
        return ToutiaoUtil.TOUTIAO_DOMAIN + "image?name=" + fileName;
    }

图片访问:

通过图片的url,服务器使用StreamUtils.copy()将对应的图片二进制流,copy到response的输出流中。


    @RequestMapping(path = {"/image"}, method = {RequestMethod.GET})
    @ResponseBody
    public void getImage(@RequestParam("name") String imageName,
                         HttpServletResponse response) {
        try {
            response.setContentType("image/jpeg");
            StreamUtils.copy(new FileInputStream(new
                    File(ToutiaoUtil.IMAGE_DIR + imageName)), response.getOutputStream());
        } catch (Exception e) {
            logger.error("读取图片错误" + imageName + e.getMessage());
        }
    }

二、上传到七牛云,做云存储 
优势:图片做单独的服务器 
cdn内容分发网络,内容分发到各个节点,能够更快的访问静态文件。 
云可以做实时缩图和实时切图。 
七牛云上传图片直接写成一个service,如果要更换云服务商,如阿里云,可以直接改service就行。
 

2.3.4 发布news和comment

/user/addNews/:用户发布news

如果用户为登录用户(hostHolder.getUser()!=null),则该news设定为对应的用户,;

如果用户为未登录用户,则设定为匿名用户(news.setUserId(3);)

 

/news/{newsId}:newsid对应的news详情页;

1. news内容:获取该newsId对应的news内容;

2. 赞的状态:用户为登录用户,获取用户对该news的赞状态;

3. 评论:获取该news对应的评论;

 

/addComment:添加newsId对应的一个comment;

1. 添加comment;

2. 更新news相关的评论数目;

 

2.3.5 Redis实现点赞点踩

使用redis的set数据结构实现;

好处:Redis性能好,快,并发高;在点赞过后要立即刷新显示在页面,所以推荐使用Redis。

RedisUtil类中方法:

  1. getLikeKey(int entityId, int entityType):获取like对应redis中的key,如:LIKE:1:1234(1代表news业务,1234代表的是指定的newsId);
  2. getDisLikeKey(int entityId, int entityType):获取disike对应redis中的key,如:DISLIKE:1:1234

思路:

  • 点赞:将当前用户userid作为value,存入到对应like集合当中,同时判断点dislike集合中是否有此id值,有的话就移除; 
  • 点踩:与上面操作相反。
  • 查看用户点赞点踩状态:通过like和dislike对应的key,以及userid,查找userid是否在对应的集合中;

 

2.3.6 异步处理

消息队列MQ主要是用来:

  • 解耦应用、
  • 异步化消息
  • 流量削峰填谷

目前使用的较多的有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。

Redis不仅可作为缓存服务器,还可用作消息队列。它的list类型天生支持用作消息队列。list是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的。

为什么要用Redis实现轻量级MQ?

在业务的实现过程中,就算没有大量的流量,解耦和异步化几乎也是处处可用,此时MQ就显得尤为重要。但与此同时MQ也是一个蛮重的组件,例如我们如果用RabbitMQ就必须为它搭建一个服务器,同时如果要考虑可用性,就要为服务端建立一个集群,而且在生产如果有问题也需要查找功能。在中小型业务的开发过程中,可能业务的其他整个实现都没这个重。过重的组件服务会成倍增加工作量。
所幸的是,Redis提供的list数据结构非常适合做消息队列。

 

Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。调用toJSONString方 法即可将对象转换成 JSON 字符串,parseObject 方法则反过来将 JSON 字符串转换成对象

牛客网中级项目笔记_第6张图片

  • EventModule类:代表每个事件具体的信息;
  • EventProducer类:生产者,生成一个EventModule对象,将该对象存储在Redis list对应的key中,等待Event Consumer消费;
  • EventConsumer类:消费者,启动一个线程,从list中读取EventModule对象,然后找到对应的EventHandle对象,处理对应的事件。
  • EventHandle接口:处理不同事件,有不同的handle实现;
    • LikeHandle:对应处理EventType为LIKE的事件;
    • CommentHandle:对应处理EventType为COMMENT的事件;
    • MailHandle:对应处理EventType为MAIL的事件;

EventProducer:生产方

@Service
public class EventProducer {
    @Autowired
    JedisAdapter jedisAdapter;

    public boolean fireEvent(EventModel model) {
        try {
            String json = JSONObject.toJSONString(model);
            String key = RedisKeyUtil.getEventQueueKey();
            jedisAdapter.lpush(key, json);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

 

EventConsumer类:消费者

  1. 获取不同的EventHandler的实现类,如LikeHandler、CommentHandler、MailHandler等;
Map beans = applicationContext.getBeansOfType(EventHandler.class);

    2. 根据EventHandler的实现类,得到每个EventType对应的处理器(EventHandle)是哪些

    3. 新建线程,while(true)循环使得后台一直有一个线程在消费队列。 从list中读取对应key下的事件(即EventModule对象),根据该EventModule对象的EventType,调用对应的EventHandle来处理事件

@Service
public class EventConsumer implements InitializingBean, ApplicationContextAware {
    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
    private Map> config = new HashMap>();
    private ApplicationContext applicationContext;

    @Autowired
    JedisAdapter jedisAdapter;

    @Override
    public void afterPropertiesSet() throws Exception {
        //获取EventHandler类所有的实现类的bean,返回一个map类型的实例,map中的key为bean的名字,key对应的内容未bean的实例。
        Map beans = applicationContext.getBeansOfType(EventHandler.class);
        if (beans != null) {
            //得到每个EventType对应的EventHandle
            for (Map.Entry entry : beans.entrySet()) {
                List eventTypes = entry.getValue().getSupportEventTypes();
                for (EventType type : eventTypes) {
                    if (!config.containsKey(type)) {
                        config.put(type, new ArrayList());
                    }
                    config.get(type).add(entry.getValue());
                }
            }
        }

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    String key = RedisKeyUtil.getEventQueueKey();
                    List events = jedisAdapter.brpop(0, key);
                    for (String message : events) {
                        if (message.equals(key)) {
                            continue;
                        }

                        EventModel eventModel = JSON.parseObject(message, EventModel.class);

                        if (!config.containsKey(eventModel.getType())) {
                            logger.error("不能识别的事件");
                            continue;
                        }

                        for (EventHandler handler : config.get(eventModel.getType())) {
                            handler.doHandle(eventModel);
                        }
                    }
                }
            }
        });
        thread.start();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

InitializingBean接口:afterPropertiesSet方法,初始化bean的时候执行,可以针对某个具体的bean进行配置;实现 InitializingBean接口必须实现afterPropertiesSet方法。

ApplicationContextAware接口:某些特殊的情况下,Bean需要实现某个功能,但该功能必须借助于Spring容器才能实现,此时就必须让该Bean先获取Spring容器,然后借助于Spring容器实现该功能。为了让Bean获取它所在的Spring容器,可以让该Bean实现ApplicationContextAware接口。

详见:Bean的生命周期

 

 

 

你可能感兴趣的:(Springboot)