问答网站设计

WenDa网站设计

数据库设计

CREATE SCHEMA `wenda` DEFAULT CHARACTER SET utf8;
DROP TABLE IF EXISTS `question`;
CREATE TABLE `question` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL,
  `content` TEXT NULL,
  `user_id` INT NOT NULL,
  `created_date` DATETIME NOT NULL,
  `comment_count` INT NOT NULL,
  PRIMARY KEY (`id`),
  INDEX `date_index` (`created_date` ASC));

  DROP TABLE IF EXISTS `user`;
  CREATE TABLE `user` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(64) NOT NULL DEFAULT '',
    `password` varchar(128) NOT NULL DEFAULT '',
    `salt` varchar(32) NOT NULL DEFAULT '',
    `head_url` varchar(256) NOT NULL DEFAULT '',
    PRIMARY KEY (`id`),
    UNIQUE KEY `name` (`name`)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  DROP TABLE IF EXISTS `login_ticket`;
  CREATE TABLE `login_ticket` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `user_id` INT NOT NULL,
    `ticket` VARCHAR(45) NOT NULL,
    `expired` DATETIME NOT NULL,
    `status` INT NULL DEFAULT 0,
    PRIMARY KEY (`id`),
    UNIQUE INDEX `ticket_UNIQUE` (`ticket` ASC)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  DROP TABLE IF EXISTS `comment`;
  CREATE TABLE `comment` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `content` TEXT NOT NULL,
  `user_id` INT NOT NULL,
  `entity_id` INT NOT NULL,
  `entity_type` INT NOT NULL,
  `created_date` DATETIME NOT NULL,
  `status` INT NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  INDEX `entity_index` (`entity_id` ASC, `entity_type` ASC)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  DROP TABLE IF EXISTS `message`;
  CREATE TABLE `message` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `from_id` INT NULL,
    `to_id` INT NULL,
    `content` TEXT NULL,
    `created_date` DATETIME NULL,
    `has_read` INT NULL,
    `conversation_id` VARCHAR(45) NOT NULL,
    PRIMARY KEY (`id`),
    INDEX `conversation_index` (`conversation_id` ASC),
    INDEX `created_date` (`created_date` ASC))
  ENGINE = InnoDB
  DEFAULT CHARACTER SET = utf8;

  DROP TABLE IF EXISTS `feed`;
  CREATE TABLE `feed` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `created_date` DATETIME NULL,
    `user_id` INT NULL,
    `data` TINYTEXT NULL,
    `type` INT NULL,
    PRIMARY KEY (`id`),
    INDEX `user_index` (`user_id` ASC))
  ENGINE = InnoDB
  DEFAULT CHARACTER SET = utf8;

MyBatis集成

  • application.properties 配置

    spring.datasource.url=jdbc:mysql://localhost:3306/wenda?useUnicode=true&characterEncoding=utf8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=qian1998
    mybatis.config-location=classpath:mybatis-config.xml
    
  • 导入依赖

    <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.0.1</version>
            </dependency>
    
  • Resources目录下新建mybatis-config.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    
        <settings>
            <!-- Globally enables or disables any caches configured in any mapper under this configuration -->
            <setting name="cacheEnabled" value="true"/>
            <!-- Sets the number of seconds the driver will wait for a response from the database -->
            <setting name="defaultStatementTimeout" value="3000"/>
            <!-- Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn -->
            <setting name="mapUnderscoreToCamelCase" value="true"/>
            <!-- Allows JDBC support for generated keys. A compatible driver is required.
            This setting forces generated keys to be used if set to true,
             as some drivers deny compatibility but still work -->
            <setting name="useGeneratedKeys" value="true"/>
        </settings>
    
        <!-- Continue going here -->
    
    </configuration>
    

工具类

  • WendaUtil

    public class WendaUtil {
        private static final Logger logger = LoggerFactory.getLogger(WendaUtil.class);
    
        public static int ANONYMOUS_USERID = 3;     //匿名用户id
        public static int SYSTEM_USERID = 4;
    
        public static String getJSONString(int code) {
            JSONObject json = new JSONObject();
            json.put("code", code);
            return json.toJSONString();
        }
    
        public static String getJSONString(int code, String msg) {
            JSONObject json = new JSONObject();
            json.put("code", code);
            json.put("msg", msg);
            return json.toJSONString();
        }
    
        public static String getJSONString(int code, Map<String, Object> map) {
            JSONObject json = new JSONObject();
            json.put("code", code);
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                json.put(entry.getKey(), entry.getValue());
            }
            return json.toJSONString();
        }
    
        public static String MD5(String key) {
            char hexDigits[] = {
                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
            };
            try {
                byte[] btInput = key.getBytes();
                // 获得MD5摘要算法的 MessageDigest 对象
                MessageDigest mdInst = MessageDigest.getInstance("MD5");
                // 使用指定的字节更新摘要
                mdInst.update(btInput);
                // 获得密文
                byte[] md = mdInst.digest();
                // 把密文转换成十六进制的字符串形式
                int j = md.length;
                char str[] = new char[j * 2];
                int k = 0;
                for (int i = 0; i < j; i++) {
                    byte byte0 = md[i];
                    str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                    str[k++] = hexDigits[byte0 & 0xf];
                }
                return new String(str);
            } catch (Exception e) {
                logger.error("生成MD5失败", e);
                return null;
            }
        }
    }
    
  • JedisAdapter

    封装redis的一些操作,用来给其他类调用

  • RedisKeyUtil

    public class RedisKeyUtil {
        private static String SPLIT = ":";
        private static String BIZ_LIKE = "LIKE";
        private static String BIZ_DISLIKE = "DISLIKE";
        private static String BIZ_EVENTQUEUE = "EVENT_QUEUE";
        // 获取粉丝
        private static String BIZ_FOLLOWER = "FOLLOWER";
        // 关注对象
        private static String BIZ_FOLLOWEE = "FOLLOWEE";
        private static String BIZ_TIMELINE = "TIMELINE";
    
        public static String getLikeKey(int entityType, int entityId) {
            return BIZ_LIKE + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId);
        }
    
        public static String getDisLikeKey(int entityType, int entityId) {
            return BIZ_DISLIKE + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId);
        }
    
        public static String getFollowerKey(int entityType, int entityId) {
            return BIZ_FOLLOWER + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId);
        }
    
        public static String getFolloweeKey(int userId, int entityType) {
            return BIZ_FOLLOWEE + SPLIT + String.valueOf(userId) + SPLIT + String.valueOf(entityType);
      }
        public static String getEventQueueKey() {
            return BIZ_EVENTQUEUE;
        }
    }
    

SpringBoot注解

路径请求参数

//获取路径中的参数以及request请求中的参数
@RequestMapping(path = "/profile/{groupId}/{userId}",method = {RequestMethod.GET})
@ResponseBody
public String profile(@PathVariable("userId") int userId,
                      @PathVariable("groupId") String groupId,
                      @RequestParam(value = "type",defaultValue = "1") int type,
                      @RequestParam(value = "key",required = false) String key){
    return String.format("Profile page of %s / %d  type:%d key:%s",groupId,userId,type,key);
}




自定义错误页面

//自定义访问错误页面
@ExceptionHandler()
@ResponseBody
public String error(Exception e){
    return "error:"+ e.getMessage();
}

AOP切面编程

//AOP切面编程
@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Before("execution(* com.qiankai.wenda.controller.*Controller.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        StringBuilder sb = new StringBuilder();
        for (Object arg : joinPoint.getArgs()) {
            sb.append("arg:" + arg.toString() + "|");
        }
        logger.info("before method:"+sb.toString());
    }

    @After("execution(* com.qiankai.wenda.controller.IndexController.*(..))")
    public void afterMethod(){
        logger.info("after method");
    }
}

业务功能

注册

  1. 用户名合法性检查(长度,敏感词,重复,特殊字符)
  2. 密码长度要求
  3. 密码salt加密,密码强度检测(MD5库)
  4. 用户邮箱/短信激活

登陆/登出

  • 登陆

    • 服务器密码校验/第三方检验回调,token登记

      • 服务端token关联userid
      • 客户端存储token(app存储本地,浏览器存储cookie)
    • 服务端/客户端token有效期(记住登陆)

      注:token可以是sessionid,或者是cookie里的一个key

    • 流程:

      新建LoginTicket.class

      新建LoginTicketDAO

      LoginController --> UserService --> login/register(方法) --> addLoginTicket(添加ticket) -->封装ticket到map,返回给controller,存入Cookie,response.addCookie(cookie)

    • 页面访问

      • 客户端:带token的HTTP请求
      • 服务端:
        1. 根据token获取用户id
        2. 根据用户id获取用户的具体信息
        3. 用户和页面访问权限处理
        4. 渲染页面/跳转页面
  • 拦截器Interceptor

    • 新建 model --> HostHolder.class,用于存放浏览器中的用户,不同的线程获取到不同的对应的用户

      @Component
      public class HostHolder {
          private static ThreadLocal<User> users = new ThreadLocal<>();
      
          public User getUser(){
              return users.get();
          }
      
          public void setUsers(User user) {
              users.set(user);
          }
          public void clear(){
              users.remove();
          }
      }
      
    • 新建 interceptor --> PassportInterceptor.class -----获取user

      • preHandle() 查询出当前登陆的user,并存入hostHolder

        获取cookie中的ticket,通过ticket查询LoginTicket,进而查找出user = userDAO.selectById(loginTicket.getUserId());,将user存入hostHolder

        String ticket = null;
        if (request.getCookies() != null) {
            for (Cookie cookie : request.getCookies()) {
                if ("ticket".equals(cookie.getName())) {
                    ticket = cookie.getValue();
                    break;
                }
            }
        }
        if (ticket != null) {
            LoginTicket loginTicket = loginTicketDAO.selectByTicket(ticket);
            if (loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus() != 0) {
                return true;
            }
            User user = userDAO.selectById(loginTicket.getUserId());
            hostHolder.setUsers(user);
        }
        
        return true;
        
      • postHandle() 将user放入modelAndView,使页面可获得user对象

        在所有的controller渲染之前,把user加进去,使得页面可以直接获得user

        modelAndView.addObject("user", hostHolder.getUser());
        
      • afterCompletion()

        清空hostHolder

        hostHolder.clear();
        
    • 配置类configuration --> WendaWebConfiguration.class

      系统初始化时加入拦截器

    • 登陆流程

      1. 初始化拦截器PassportInterceptor
      2. 执行拦截器的preHandle()方法,将user存入hostHolder
      3. 在渲染之前执行拦截器的postHandle()方法,将hostHolder中的user加入modelAndView,使得页面可以使用user
      4. 完成后执行拦截器afterCompletion
  • 登出

    • 服务端/客户端token删除

    • session清理

    • LoginController --> logout,调用service层方法

      @RequestMapping(path = "/logout/", method = RequestMethod.POST)
      public String logout(@CookieValue("ticket") String ticket) {
          userService.logout(ticket);
          return "redirect:/";
      }
      

      userService --> logout,调用DAO层方法,更新ticket状态,使其失效(0:有效,1:失效);

      public void logout(String ticket) {
          loginTicketDAO.updateStatus(ticket,1);
      }
      
  • 未登录跳转

    • 新建拦截器interceptor -->LoginRequredInterceptor.class

      判断当前HostHolder用户是否为空,若为空,则跳转到登陆页面,并把当前页面的URI带去。

      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          if (hostHolder.getUser() == null) {
              response.sendRedirect("/reglogin?next="+request.getRequestURI());
          }
          return true;
      }
      

      将拦截器加入配置类WendaWebConfiguration中(在PassporInterceptor之后)

      @Override
          public void addInterceptors(InterceptorRegistry registry) {
              //系统初始化时加入一个拦截器
              registry.addInterceptor(passportInterceptor);
              registry.addInterceptor(loginRequredInterceptor).addPathPatterns("/user/*");//访问/user/下页面时,启用拦截器
              super.addInterceptors(registry);
          }
      
      

      处理登陆逻辑

      /*LoginController.class*/
          
      @RequestMapping(path = "/reglogin",method = RequestMethod.GET)
      public String regLogin(Model model,@RequestParam("next") String next){
          model.addAttribute("next", next);
          return "login";
      }
      
      /*将带过去的next存入表单隐藏域--->login.html
      <input type="hidden" name="next" value="$!{next}"/>
      */
      
      @RequestMapping(path = "/login/", method = RequestMethod.POST)
          public String login(Model model,
                              @RequestParam("username") String username,
                              @RequestParam("password") String password,
                              @RequestParam(value = "next",required = false) String next,
                              @RequestParam(value = "rememberme",defaultValue = "false") boolean rememberme,
                              HttpServletResponse response) {
              try {
                  Map<String, String> map = userService.login(username, password);
                  if (map.containsKey("ticket")) {
                      Cookie cookie = new Cookie("ticket",map.get("ticket"));
                      cookie.setPath("/");
                      if (rememberme) {
                          cookie.setMaxAge(3600*24*5);
                      }
                      //在这里判断是否有next,重定向回去
                      if (!StringUtils.isEmpty("next")){
                          return "redirect:"+next;
                      }
                      response.addCookie(cookie);
                      return "redirect:/";
                  }else{
                      model.addAttribute("msg", map.get("msg"));
                      return "login";
                  }
              } catch (Exception e) {
                  logger.error("登陆异常");
                  return "login";
              }
          }
      
      

发布问题

  • 添加问题QuestionController.class

    从HostHolder中获取用户信息,若用户未登陆,返回错误代码code:999,否则调用service层执行问题插入,判断是否插入成功,返回code:0

  • 过滤用户发布内容QuestionService.class

    在添加问题方法中,对用户内容进行过滤(下一标题解释)

    public int addQuestint(Question question) {
        //过滤用户内容html转义,防止其中含有恶意脚本代码
        question.setContent(HtmlUtils.htmlEscape(question.getContent()));
        question.setTitle(HtmlUtils.htmlEscape(question.getTitle()));
        //敏感词过滤
        question.setContent(sensitiveService.filter(question.getContent()));
        question.setTitle(sensitiveService.filter(question.getTitle()));
        return addQuestint(question)>0?question.getId():0;
    }
    
    

    敏感词过滤

  • 新建SensitiveService.class

    • 建立内部类,敏感词树
    private class TrieNode{
        //是否为关键词的结尾
        private boolean end = false;
        //当前节点下所有子节点
        private Map<Character,TrieNode> subNodes = new HashMap<>();
    	//向指定位置添加节点树
        public void addSubNode(Character key, TrieNode node) {
            subNodes.put(key, node);
        }
    	//获取下个节点
        TrieNode getSubNode(Character key) {
            return subNodes.get(key);
        }
    
        boolean isKeywordEnd(){
            return end;
        }
    
        void setKeywordEnd(boolean end) {
            this.end = end;
        }
    }
    
    
    • 添加关键词
    //增加关键词
    private void addWord(String lineTxt) {
        TrieNode tempNode = rootNode;
        for (int i=0; i < lineTxt.length(); i++) {
            Character c = lineTxt.charAt(i);
            TrieNode node = tempNode.getSubNode(c);
            if (node == null) {
                node = new TrieNode();
                tempNode.addSubNode(c,node);
            }
            tempNode = node;
            if (i == lineTxt.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }
    
    
    • 将该类实现InitializingBean接口,实现afterPropertiesSet()方法,用于初始化bean后执行将敏感词文件读入的功能
    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化bean的时候执行
        try {
            InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("SensitiveWords.txt");
            InputStreamReader reader = new InputStreamReader(is);
            BufferedReader bufferedReader = new BufferedReader(reader);
            String lineTxt;
            while ((lineTxt = bufferedReader.readLine()) != null) {
                addWord(lineTxt.trim());
            }
            reader.close();
        } catch (Exception e) {
            logger.error("读取敏感词文件失败"+e.getMessage());
        }
    }
    
    
    • 判断是否为符号,防止故意干扰敏感词过滤

      /**
       *  判断是否是一个符号,如空格,特殊符号,防止干扰敏感词过滤
       */
      private boolean isSymbol(char c) {
          int ic = (int) c;
          //东亚文字
          return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
      }
      
      
    • 编写过滤方法filter

      /**
       * 过滤敏感词
       * @param text
       * @return
       */
      public String filter(String text) {
          if (StringUtils.isBlank(text)) {
              return text;
          }
      
          StringBuilder result = new StringBuilder();
          String replacement = "***";
      
          TrieNode tempNode = rootNode;
          int begin = 0;
          int position = 0;
      
          while (position < text.length()) {
              char c = text.charAt(position);
              //如果是一个非法字符,就跳过,防止影响敏感词判断
              if (isSymbol(c)) {
                  if (tempNode == rootNode) {
                      //如果是一个开始判定的字符,就保存,以免丢失
                      result.append(c);
                      ++begin;
                  }
                  ++position;
                  continue;
              }
              tempNode = tempNode.getSubNode(c);
              if (tempNode == null) {
                  result.append(text.charAt(begin));
                  position = begin+1;
                  begin = position;
                  tempNode = rootNode;
              } else if (tempNode.isKeywordEnd()) {
                  //发现敏感词
                  result.append(replacement);
                  position = position+1;
                  begin =position;
                  tempNode = rootNode;
              }else{
                  ++position;
              }
          }
          result.append(text.substring(begin));
          return  result.toString();
      }
      
      

多线程

评论中心

  • 评论的类型 entity_id, entity_type

    • 回答
    • 评论/回复
  • CommentDAO

    • int addComment(Comment comment)

    • List selectCommentByEntity(@entityId,@entityType)

    • int getCommentCount(@entityId,@entityType)

    • deleteCommnet(id,status)

      @Update实现,修改status状态

  • CommentService

    • List getCommentsByEntity(entityId,entityType)

    • int addComment(comment)

      敏感词过滤再插入数据库

    • getCommentCount(entityId,entityType)

    • boolean deleteComment(commentId)

  • CommentController

    • addComment(@questionId,content)

      "addComment"

      先判断是否登陆

      更新问题评论数

      跳转回问题页

  • QuesetionController 添加展示问题数据

    • questionDetail(model,@PathVariable(qid))

      "/question/{qid}"

      获取该问题所有评论

      将每条评论信息添加到ViewObject数组comments中,再获取点赞数(redis),返回给前端

      return ”detail“

消息中心

  • MessageDAO

    • int addMessage(Message)

    • List getConversationDetail(@conversationId,@offset,@limit)

      获取每个会话的内部聊天信息

    • List getConversationList(@userId,@offset,@limit)

      获取会话列表

    • int getConversationUnreadCount(@userId,@conversation)

      select where has_read=0 and to_id=#{userId} and conversation_id = #

  • MessageService

    • int addMessage(message)
    • List getConversationDetail(conversationId,offset,limit)
    • List getConversationList(userId,offset,limit)
    • int getConversationUnreadCount(userId,conversationId)
  • MessageController

    • addMessage(@toName,@content)

      "/msg/addMessage"

      判读用户登陆

      敏感词过滤

      return WendaUtil.getJSONString()

    • getConversationList(model)

      "/msg/list" 展示会话列表

      判断用户是否登陆

      将取出的信息存入ViewObject数组conversations

      return "letter"

    • getConversationDetail(model,@conversationId)

      "/msg/detail" 展示每个会话内部的message

      取出Message,存入ViewObject

      return ”letterDetail“

Redis简介

List

双向列表,适用于最新列表,关注列表

  • lpush
  • lpop
  • blpop
  • lindex
  • lrange
  • lrem
  • linsert
  • lset
  • lpush

Hash

对象属性,不定长属性数

  • hset
  • hget
  • hgetAll
  • hexists
  • hkeys
  • hvals

Set

适用于无顺序的集合,点赞点踩,抽奖,已读,共同好友

  • sdiff
  • smembers
  • sinter
  • scard

KV

单一数值,验证码,PV,缓存

  • set
  • setex
  • incr

SortedSet

排行榜,优先队列

  • zadd
  • zscore
  • zrange
  • zcount
  • zrank
  • zrevrank

JedisAdapter

点赞/踩

  • JedisAdapter

    redis的一些操作

  • RedisKeyUtil

    用于生成redis的key值,保证不重复

    业务+:+entityType+:+entityId

  • LikeService

    • long like(userId,entityType,entityId)
    • long dislike(userId,entityType,entityId)
    • int getLikeStatus(userId,entityType,entityId)
    • long getLikeCount(entityType,entityId)
  • LikeController

    • like(@commentId)

      ”/like”----POST

      return WendaUtil.getJSONString

    • disLike(@commentId)

      "/dislike"----POST

      return WendaUtil.getJSONString

  • QuestionController

    在方法questionDetail()发送数据给前端时加上点赞数

异步队列

  • EventType 事件类型

    • LIKE
    • COMMENT
    • LOGIN
    • MAIL
    • FOLLOW
    • UNFOLLOW
  • EventModel

    • EventType type 事件类型
    • int actorId 触发事件的人
    • int entityType 事件实体类型
    • int entitiyId 事件实体id
    • int entityOwnerId 被触发事件的关联用户
    • Map<String,String> exts 封装以上属性

    set方法返回EventModel,return this;,这样以后设置起来很方便,比如直接给对象设置属性xx.setXXX().setAAA().setBBB()

  • EventProducer

    • 将eventModel加入redis队列

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

    • 处理event

    • void doHandle(EventModel model)

      处理event

    • List<EventType> getSupportEventTypes()

      找到关心的event

  • EventConsumer implements InitializingBean,ApplicationContextAware

    分发event,建立关系

    private Map<EventType,List<EventHandler>> config = new HashMap<>();

    private ApplicationContext applicationContext

    找到所有实现了EventHandler接口的实现类,存入Map

    public void afterPropertiesSet() throws Exception{
        //先通过spring找到所有的EventHandler实现类
    	Map<String,EventHandler> beans = applicationContext.getBeansOfType(EventHandler.class);
    	//如果有
        if(beans != null){
            //对每个EventHandler实现类进行遍历,看有没有要处理的事件
            for(Map.Entry<String,EventHandler> entry : beans.entrySet()){
                //找出当前遍历的EventHandler下的所有支持的事件类型
                List<EventType> eventTypes = entry.getValue().getSupportEventTypes();
                //对当前EventHandler支持的不同类型的事件进行遍历
                for(EventType type : eventTypes){
                    if(!config.containsKey(type)){
                        //config中没有,就先初始化一个
                        config.put(type,new ArrayList<EventHandler>());
                    }
                    //将该类型对应的具体EvnetHandler实现类存入config对应的type中
                    config.get(tyep).add(entry.getValue());
                }
            }
        }
        //开启一个线程处理Handler
        Thread thread = new Thread(new Runnable(){
            @override
            public void run(){
                while(true){
                    String key = RedisKeyUtil.getEventQueueKey();
                    List<String> events = jedisAdapter.brpop(0,key);
                    //取出序列化成String的事件
                    for(String message : events){
                        if(message.equals(key)){
                            //将第一个key过滤掉
                            continue;
                        }
                        //将message反序列化为对象
                        EventModel eventModel = JSON.parseObject(message,EventModel.class);
                        //去掉非法事件
                        if(!config.containKey(eventModel.getType())){
                            logger.error("不能识别的事件");
                                continue;
                        }
                        
                        for(Eventhandler handler : config.get(eventModel.getType())){
                            //处理事件
                            handler.doHandler(eventModel);
                        }
                    }
                }
            }
        });
        thread.start();
    }
    
    
  • LikeHandler implements EventHandler

    public void doHandle(EventModel model){
        Message message = new Message();
        message.setFromId(WendaUtil.SYSTEM_USERID);
        message.setToId(model.getEntityOwnerId());
        message.setCreateDate(new Date());
        User user = userService.getUser(model.getActorId());
        message.setContent(".."+user.getName()+".."+modle.getExt("questionId"));
        
        messageService.addMessage(message);
    }
    
    public List<EventType> getSupportEventTypes(){
        return Arrays.asList(EventType.LIKE);
    }
    
    

    LikeController

    @RequestMapping(...)
    @ResponseBody
    public String like(...){
    	//...
    	eventProducer.fireEvent(new EventModel(EventType.LIKE)
                               .setActorId(hostHolder.getUser.getId())
                               .setEntityId(commentId)
                                .setEntityType(Entity.ENTITY_COMMENT)
                                .setEntityOwerId(comment.getUserId())
                               .setExt("questionId",String.valueOf(comment.getEntityId())));
       
        //...
    }
    
    

邮件

  • 导入依赖

    <dependency>
        <groupId>javax.mail</groupId>
        <artifactId>mail</artifactId>
        <version>1.4.7</version>
    </dependency>
    
    
  • 用户登陆异常,发送邮件给用户

  • MailSender

    @Service
    public class MailSender implements InitializingBean {
        private static final Logger logger = LoggerFactory.getLogger(MailSender.class);
        private JavaMailSenderImpl mailSender;
    
        @Autowired
        private VelocityEngine velocityEngine;
    
        public boolean sendWithHTMLTemplate(String to, String subject,
                                            String template, Map<String, Object> model) {
            try {
                String nick = MimeUtility.encodeText("牛客中级课");
                InternetAddress from = new InternetAddress(nick + "<course@nowcoder.com>");
                MimeMessage mimeMessage = mailSender.createMimeMessage();
                MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
                String result = VelocityEngineUtils
                        .mergeTemplateIntoString(velocityEngine, template, "UTF-8", model);
                mimeMessageHelper.setTo(to);
                mimeMessageHelper.setFrom(from);
                mimeMessageHelper.setSubject(subject);
                mimeMessageHelper.setText(result, true);
                mailSender.send(mimeMessage);
                return true;
            } catch (Exception e) {
                logger.error("发送邮件失败" + e.getMessage());
                return false;
            }
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            mailSender = new JavaMailSenderImpl();
            mailSender.setUsername("course@nowcoder.com");
            mailSender.setPassword("NKnk123");
            mailSender.setHost("smtp.exmail.qq.com");
            //mailSender.setHost("smtp.qq.com");
            mailSender.setPort(465);
            mailSender.setProtocol("smtps");
            mailSender.setDefaultEncoding("utf8");
            Properties javaMailProperties = new Properties();
            javaMailProperties.put("mail.smtp.ssl.enable", true);
            //javaMailProperties.put("mail.smtp.auth", true);
            //javaMailProperties.put("mail.smtp.starttls.enable", true);
            mailSender.setJavaMailProperties(javaMailProperties);
        }
    }
    
    
    
  • LoginExceptionHandler

关注服务

不使用数据库存储,使用redis实现

没有dao层,数据操作在JedisAdapter中实现

  • FollowService

    • follow(userId,entityType,entityId)

      先通过RedisKeyUtil工具类获取到粉丝和关注对象的key,通过jedis开启事务

      将(entityType,entityId)实体类的粉丝集合followerKey中,添加(userId)当前用户

      再将当前用户的关注集合followeeKey对这类的实体的关注加一,并返回事务是否成功

    • unfollow(userId,entityType,entityId)

      同上,取消关注

    • getFollowers(entityType,entityId,count)

    • getFollowers(entityType,entityId,offset,count)

      获取实体粉丝列表

    • getFollowees(entityType,entityId,count)

    • getFollowees(entityType,entityId,offset,count)

      获取关注的实体列表

    • getFollowerCount(entityType,entityId)

      获取实体粉丝数

    • getFolloweeCount(userId,enitityType)

      获取用户关注指定的实体类型的数目

    • isFollower(userId,entityType,entityId)

      判断用户是否关注了某个实体

  • FollowController

    • followUser(@RequsetParam("userId"))

      关注用户,并通过异步队列,将关注信息发给被关注对象,返回关注人数

    • unfollowUser(@RequestParam("userId"))

      取消关注用户

    • followQuestion(@RequestParam("questionId"))

      关注问题

    • unfollowQuestion(@RequestParam("questionId"))

      取消关注问题

    • followers(model,@PathVariable("uid"))

      获取当前查看的用户,以及该用户的粉丝列表和粉丝数

    • followees(model,@PathVariable("uid"))

      获取当前查看的用户,以及该用户关注的人和个数

TimeLine

推拉模式

  • Feed

    • id
    • type 事件的类型,是评论还是关注
    • userId 事件发起的人
    • data 事件内容
    • dataJSON 将事件内容保存为JSON,便于转换为对象
  • FeedDAO

    • addFeed 添加一个新鲜事
    • getFeedById 推送push新鲜事
    • selectUserFeeds 拉取pull新鲜事
  • FeedService

    • getUserFeeds 拉取新鲜事
    • addFeed 添加新鲜事
    • getById 推送新鲜事
  • FeedController

    • getPushFeeds 推送

      先判断当前用户是否登陆通过当前登陆的用户id,获取新鲜事列表

    • getPullFeeds 拉取
      判断当前用户是否登陆,根据当前登陆用户id获取关注的人列表,再获取他们的动态

全文搜索Solr

posted @ 2019-12-24 21:17  它山之玉  阅读(166)  评论(0编辑  收藏  举报