【项目笔记】
仿牛客网社区项目,主页显示大家发布的贴子。我发现难的是前端怎么写。。。。
先进行数据库设计:
用户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); }