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
导入依赖
mysql mysql-connector-java runtime org.mybatis.spring.boot mybatis-spring-boot-starter 2.0.1 Resources目录下新建mybatis-config.xml
工具类
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
map) { JSONObject json = new JSONObject(); json.put("code", code); for (Map.Entry 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");
}
}
业务功能
注册
- 用户名合法性检查(长度,敏感词,重复,特殊字符)
- 密码长度要求
- 密码salt加密,密码强度检测(MD5库)
- 用户邮箱/短信激活
登陆/登出
登陆
服务器密码校验/第三方检验回调,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请求
- 服务端:
- 根据token获取用户id
- 根据用户id获取用户的具体信息
- 用户和页面访问权限处理
- 渲染页面/跳转页面
拦截器Interceptor
新建 model --> HostHolder.class,用于存放浏览器中的用户,不同的线程获取到不同的对应的用户
@Component public class HostHolder { private static ThreadLocal
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
系统初始化时加入拦截器
登陆流程
- 初始化拦截器PassportInterceptor
- 执行拦截器的preHandle()方法,将user存入hostHolder
- 在渲染之前执行拦截器的postHandle()方法,将hostHolder中的user加入modelAndView,使得页面可以使用user
- 完成后执行拦截器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 */ @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
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
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 = #{conversationId}
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
- FOLLOW
- UNFOLLOW
EventModel
- EventType type 事件类型
- int actorId 触发事件的人
- int entityType 事件实体类型
- int entitiyId 事件实体id
- int entityOwnerId 被触发事件的关联用户
- Map
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
getSupportEventTypes() 找到关心的event
EventConsumer implements InitializingBean,ApplicationContextAware
分发event,建立关系
private Map
> config = new HashMap<>(); private ApplicationContext applicationContext
找到所有实现了EventHandler接口的实现类,存入Map
public void afterPropertiesSet() throws Exception{ //先通过spring找到所有的EventHandler实现类 Map
beans = applicationContext.getBeansOfType(EventHandler.class); //如果有 if(beans != null){ //对每个EventHandler实现类进行遍历,看有没有要处理的事件 for(Map.Entry entry : beans.entrySet()){ //找出当前遍历的EventHandler下的所有支持的事件类型 List eventTypes = entry.getValue().getSupportEventTypes(); //对当前EventHandler支持的不同类型的事件进行遍历 for(EventType type : eventTypes){ if(!config.containsKey(type)){ //config中没有,就先初始化一个 config.put(type,new ArrayList ()); } //将该类型对应的具体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 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
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()))); //... }
邮件
导入依赖
javax.mail mail 1.4.7 用户登陆异常,发送邮件给用户
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
model) { try { String nick = MimeUtility.encodeText("牛客中级课"); InternetAddress from = new InternetAddress(nick + " "); 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("[email protected]"); 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获取关注的人列表,再获取他们的动态