Java项目笔记之旅游点评项目总结03

末鹿安然 提交于 2021-02-10 09:49:39


不点蓝字,我们哪来故事?



游记:

游记是用户自己发表的。由用户自己管理,管理人员只负责审核和发布;


需求:查看和拒绝

用户的文章由前端用户自己维护,管理仅仅显示和对状态进行管理,不能进行添加编辑操作;

查看是前台只需要游记的内容即可,后台将游记的内容反给前台。根据当前游记的id查到游记对象,在从游记对象中getContent()返回即可;


审核状态:

审核逻辑:游记满足什么条件才进行审核,审核通过和审核不通过分别需要做什么操作?

  • 审核通过是发布状态;

  • 只对状态是待审核的游记才进行审核;

  • 审核通过/拒绝将游记的状态改成审核通过/审核拒绝;

  • 审核通过之后还需要改变游记的发布时间和最后修改时间;

  • 用户修改内容之后,需要考虑那些统计数据(点赞阅读等)要不要清空(问你的产品经理);

     //审核游记     @Override     public void changeState(String id, int state) throws LoadException {         //满足什么条件才进行审核         //查询游记         Optional<Travel> optional = repository.findById(id);         if (!optional.isPresent() && state != Travel.STATE_WAITING) {             //有内容并且状态是待审核的才审核,否则抛异常             throw new LoadException("该游记不可符合发布条件!");         }          //审核通过做什么,审核通过之后,游记的字段需要更新的字段有:releaseTime  lastUpdateTime  state         Travel travel = new Travel();         travel.setId(id);         travel.setLastUpdateTime(new Date());         Query query = new Query();         //惨痛——防止全部更新         query.addCriteria(Criteria.where("_id").is(id));         if (state == Travel.STATE_RELEASE) {             //1待审核             travel.setState(Travel.STATE_RELEASE);//通过             travel.setReleaseTime(new Date());//发布时间         } else {             //审核拒绝要做什么             travel.setState(Travel.STATE_REJECT);//拒绝             travel.setReleaseTime(null);//发布时间         }         //更新         DBHelper.update(template, query, travel,                 "state", "releaseTime", "lastUpdateTime");     }

 

游记明细:

明细中可以以创建时间/热门(浏览量),人均消费,出行天数做为分页查询的条件;


范围查询的处理

范围查询:前台设计成来是键值对的方式;一个int类型对应着一个范围。


问题:映射关系的k-v对怎么实现?

现在传给后台的是dayType=value这样的数据,这样后台是怎么查的呢?怎样才能给一个值之后,后台的查询是用的范围条件查询?

需求逻辑:

用户选择一个查询条件,它是一个范围值,用户选择之后,将范围对应的dayType = 2传到后台,后台需要通过传过来的一个数字的到它对应的查询范围是[4, 7],然后解析范围条件中的min = 4max = 7,之后才能使用最大最小值来拼接出查询语句。

思考:

  1. MapK-V对来做映射关系。但是:map中的value不能放范围!

  2. 用对象来对范围进行封装;


浏览器发起请求之后,带到前台的参数包括了:目的地、人均消费、旅游天数、排序类型、当前页。

要将传过来的条件参数封装到query中;

封装范围条件的类:
 /**  * 游记范围查询条件:封装键值对的映射关系  */ @Getter public class TravelCondition {     //映射数据初始化k-v对,     public static final Map<Integer,TravelCondition> MAP_PEREXPEND = new HashMap<>();//人均消费     public static final Map<Integer,TravelCondition> MAP_DAY = new HashMap<>();//旅游天数      //用静态代码块将 表示范围 的数据初始化     static {         MAP_PEREXPEND.put(1, new TravelCondition(1,999));         MAP_PEREXPEND.put(2, new TravelCondition(1000,6000));         MAP_PEREXPEND.put(3, new TravelCondition(6001,20000));         MAP_PEREXPEND.put(4, new TravelCondition(20001,Integer.MAX_VALUE));          MAP_DAY.put(1, new TravelCondition(0,3));         MAP_DAY.put(2, new TravelCondition(4,7));         MAP_DAY.put(3, new TravelCondition(8,14));         MAP_DAY.put(4, new TravelCondition(15,Integer.MAX_VALUE));     }      //最大最小值     private int min;     private int max;      //构造器     public TravelCondition(int min, int max){         this.min = min;         this.max = max;     } }
游记高级查询条件:
 // 游记高级分页查条件 @Getter @Setter public class TravelQuery extends QueryObject {     private String destId;//目的地id     private int orderType = 1; //默认按最新排序     private int perExpendType = -1;//默认值根据页面定,无条件     private int dayType = -1;//默认值根据页面定,无条件      //前台传过来一个值,我们以这个值做为map的key,去得到map的value(即范围)      //因为map里的数据范围已经在静态代码块中先初始化好了,现在去拿才能拿到     //掌握了Map的key,去取value不就是直接 .get(key)      //2————> new TravelCondition(1000,6000)     public TravelCondition getPerExpend(){         return TravelCondition.MAP_PEREXPEND.get(perExpendType);     }     //2————>new TravelCondition(4,7)     public TravelCondition getDay(){         return TravelCondition.MAP_DAY.get(dayType);     } }

对应关系是这样的:


排序条件(最新/最热):

在TravelQuery中定义好了,默认按照最新(发布时间排序),如果选了最热,就按照浏览量来进行排序;


业务方法query:
     @Override     public Page<Travel> query(TravelQuery qo) {         Query query = new Query();          //目的地的分页条件         if (StringUtils.hasLength(qo.getDestId())) {             query.addCriteria(Criteria.where("destId").is(qo.getDestId()));         }          TravelCondition perExpend = qo.getPerExpend();         TravelCondition day = qo.getDay();         //人均消费的范围查询         if (perExpend != null) {             query.addCriteria(Criteria.where("perExpend").gte(perExpend.getMin()).lte(perExpend.getMax()));         }         //旅游天数的范围查询         if (day != null) {             query.addCriteria(Criteria.where("day").gte(day.getMin()).lte(day.getMax()));         }          // 1:代表默认排序:按时间排序 2:代表按浏览数排序         if (qo.getOrderType() == 2) {             qo.setPageable(PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize(), Sort.Direction.DESC, "viewnum"));         } else {             qo.setPageable(PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize(), Sort.Direction.DESC, "createTime"));         }           Page<Travel> page = DBHelper.query(template, Travel.class, query, qo.getPageable());          //缺少用户对象,前台拿用户的信息报错         for (Travel travel : page.getContent()) {//编辑分页结果的内容即可             travel.setAuthor(userInfoService.get(travel.getUserId()));//设置用户信息         }         return page;     }


游记首页:


游记添加/编辑:

添加编辑游记,需要绑定用户的id,需要用户登录只有才能保存。

查目的地不建议直接list(),应该根据分层的显示:国——省——城

     //编辑     @GetMapping("/input")     public Object input(String id) {         ParamMap map = new ParamMap();         //map.tv         if (StringUtils.hasLength(id)) {             Travel travel = travelService.get(id);             map.put("tv", travel);         }         //map.dests;         List<Destination> dests = destinationService.list();         map.put("dests", dests);         return JsonResult.success(map);     }


封面的图片上传:
 @Controller public class UploadController {      //百度富文本编辑器的图片上传     @RequestMapping("/coverImageUpload")     @ResponseBody     public Object uploadImg(MultipartFile pic) {         //将pic数据存储到项目文件中         //通过oss提供的api将图片数据上传到阿里云的oss服务器中         String url = null;         try {             url = UploadUtil.uploadAli(pic);         } catch (Exception e) {             e.printStackTrace();         }         return url;     }  }


游记的保存或者修改:

注意需要维护的数据;

这个方法让登陆过的用户才能调用,拦截了没登录的用户,不让没登录的人进入方法做操作;

需要通过HttpServletRequest请求头信息拿到token,它不能在业务层接受HttpServletRequest来做处理,他应该是表现层的事,这样高耦合了。所以选择在在controller中处理验证用户是否登录。让它们层次分明:

     //保存或者修改     @PostMapping("/saveOrUpdate")     @RequireLogin  //这个方法只能是登录的用户才能操作,所以用登录拦截器来限制未登录用户的操作     public Object saveOrUpdate(Travel travel, HttpServletRequest request) {         //保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录         //获得当前登录的用户信息,通过令牌token         String token = request.getHeader("token");         //判断用户是否登录         UserInfo user = userInfoRedisService.getUserInfoByToken(token);         if (user != null) {             travel.setUserId(user.getId());         }          //最新刚刚保存/修改的游记的id         String id = travelService.saveOrUpdate(travel);         return JsonResult.success(id);     } 
     //保存或者修改游记     @Override     public String saveOrUpdate(Travel travel) {         //保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录          //注意除了前台传过来的数据,还有以下数据真实我们在保存/修改的时候进行维护的         if (!StringUtils.hasLength(travel.getId())) {             //添加             travel.setId(null);             //添加的时候需要添加创建时间             travel.setCreateTime(new Date());             repository.save(travel);         } else {             //编辑             Query query = new Query();             query.addCriteria(Criteria.where("_id").is(travel.getId()));             //修改需要维护的字段             DBHelper.update(template, query, travel,                     "destId", "destName", "title", "coverUrl",                     "travelTime", "perExpend", "day", "person",                     "lastUpdateTime", "isPublic", "state",                     "summary", "content");         }        /*  //缺少用户对象,前台拿用户的信息报错  不在这里做         UserInfo userInfo = userInfoService.get(travel.getUserId());         travel.setAuthor(userInfo);*/          return travel.getId();     }


自定义参数解析器,实现用户对象的注入:

用户对象注入:

每次获取当前登录用户的信息都需要使用request来获取token,在通过token来获取用户信息;简单办法:通过参数注入的方式实现用户对象的注入

之前判断用户登录的在做发:

    //保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录        //获得当前登录的用户信息,通过令牌token        String token = request.getHeader("token");        //判断用户是否登录        UserInfo user = userInfoRedisService.getUserInfoByToken(token);        if (user != null) {            travel.setUserId(user.getId());        }

我希望或取用户信息遍的这样简单:

直接通过controller方法的形式参数直接得到用户的信息。这样前台需要什么信息我们都可以通过userInfo.getXxx()了。

@UserParam自定义注解用来去分参数使用自定义注解解析器;详细在后面。

    //自定义参数解析器测试    @GetMapping("/info")    public Object info(@UserParam UserInfo userInfo) {        System.err.println(userInfo);        return JsonResult.success(userInfo);    }
怎么实现:

需要使用springmvc参数解析器,(以前的用户参数解析器:文件上传参数解析器),springmvc没有提供当前用户的参数解析器,需要我们自定义;

自定义参数解析器,在请求方法的参数中自动注入当前登录用户;

自定义参数解析器:

作用:能将请求映射方法的形式参数中的UserInfo对象转换成当前登录用户对象;

前提

继承HandlerMethodArgumentResolver类,实现两个方法supportParameter() resolveArgument()

/** * 自定义参数解析器:在请求方法的参数中自动注入当前登录用户 */public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired private IUserInfoRedisService userInfoRedisService;
//表示该参数解析器支持什么类型的参数,这里支持UserInfo类型的参数 //如果该方法返回true,表示UserInfoArgumentResolver支持对UserInfo类型的参数进行解析 @Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.getParameterType() == UserInfo.class; }
//执行解析逻辑,请求映射方法的形参UserInfo对象转换成当前用户的登录对象 //上面的方法返回true,才会执行这一个方法 @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
//请求头 —— token —— userInfo HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);//通过反射拿到请求对象 String token = request.getHeader("token");//通过令牌拿到当前登录用户信息 return userInfoRedisService.getUserInfoByToken(token); }}
使用参数解析器:

在配置启动类中配置用户的参数解析器:

    //用户的参数解析器    @Bean    public UserInfoArgumentResolver userInfoArgumentResolver() {        return new UserInfoArgumentResolver();    }
//将自定义参数解析器键入到springmvc 中 public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(userInfoArgumentResolver()); }
测试:

从前台的页面发起一个ajax请求,查看回调函数中返回的数据是否有值:

        ajaxGet("/travel/info",{},function (data) {            console.log("自定义解析器");            console.log(data);        })
参数解析器实现原理:

原来的参数解析器怎么不要定义:SpringMVC中对json形式和表单形式的参数解析器都有实现,因而不需要我们自己去实现具体参数解析。

详细的解释:https://www.cnblogs.com/w-y-c-m/p/8443892.html

流程

  1. 页面请求进入到请求映射方法时,此时的springmvc会调用所有参数解析器(包括自定义的参数解析器)对请求方法中的形式参数尝试进行解析;

  2. 所有解析器遍历执行suppertsPramenter方法,如果某个参数类型使解析器的suppertsPramenter方法返回true,name终止遍历,使用该解析器的resolveArgument方法对参数进行解析;

  3. 此时:遍历到了自定义参数解析器,UserInfoResolver的suppertsPramenter方法返回true,表示这个解析器支持解析UserInfo类型的参数;

  4. springmvc使用这个解析器来实现这个参数的解析,具体的实现就是让UserInfoResolver的对象resolveArgument,将返回值注入到请求方法中的UserInfo对象中。


定义注解标记:

问题:springmvc中有默认的参数解析器,怎么区分让它用自定义的参数解析器,还是默认的参数解析器:

标记区分:注解

/** * 用户参数的注解,用于标记自定义注解解析UserInfo类型的controller形式参数 */@Target({ElementType.PARAMETER}) //贴在方法的形式参数上@Retention(RetentionPolicy.RUNTIME) //有效期:运行时public @interface UserParam {}
修改参数解析器的解析条件:
    //表示该参数解析器支持什么类型的参数,这里支持UserInfo类型的参数    //如果该方法返回true,表示UserInfoArgumentResolver支持对UserInfo类型的参数进行解析    @Override    public boolean supportsParameter(MethodParameter methodParameter) {        //条件:类型为UserInfo并且带有@UserParam注解的参数才会解析        return methodParameter.getParameterType() == UserInfo.class                && methodParameter.hasMethodAnnotation(UserParam.class);    }


游记详情:


关联目的地

查询当前篇游记的目的地, 关联查询目的地的父目的地, 显示当前目的地的封面与名称

关联的攻略

根据当前篇游记的目的地查询该目的地下阅读量为前3的攻略,循环播放

关联的游记

根据当前篇游记的目的地查询该目的地下阅读量为前3的游记,循环播放

游记评论

这里评论采用的是一级评论方式显示, 但可以使用引用方式显示上一级评论

controller :
    //游记详情    @GetMapping("/detail")    public Object detail(String id) {        // vue.detail = map.detail;        Travel travel = travelService.get(id);
// vue.toasts = map.toasts; //查询当前篇游记的目的地, 关联查询目的地的父目的地, 显示当前目的地的封面与名称 List<Destination> toasts = destinationService.getToasts(travel.getDestId());
// vue.strategies = map.strategies; //根据当前篇游记的目的地查询该目的地下阅读量为前3的攻略,循环播放 List<Strategy> strategies = strategyService.getViewnumTop3(travel.getDestId());
// vue.travels = map.travels; //根据当前篇游记的目的地查询该目的地下阅读量为前3的游记,循环播放 List<Travel> travels = travelService.getViewnumTop3(travel.getDestId());
// vue.page = map.page; 评论分页 //评论的点赞要分页 //查询评论数据,一页显示 TravelCommentQuery qo = new TravelCommentQuery(); qo.setTravelId(id); qo.setPageSize(Integer.MAX_VALUE);//一页显示所有数据21亿条 Page<TravelComment> page = travelCommentService.query(qo);

return JsonResult.success(new ParamMap() .put("detail", travel) .put("toasts",toasts ) .put("strategies",strategies ) .put("travels", travels) .put("page", page) ); }
strategyService.getViewnumTop3方法:
    //根据目的地的id查询阅读量前三的攻略    @Override    public List<Strategy> getViewnumTop3(String destId) {        //点击量前三        PageRequest of = PageRequest.of(0, 3, Sort.Direction.DESC, "viewnum");        return repository.findByDestId(destId, of);    }
travelService.getViewnumTop3方法:
    //通过目的地id查阅读量前三的游记    @Override    public List<Travel> getViewnumTop3(String destId) {        PageRequest of = PageRequest.of(0, 3, Sort.Direction.DESC, "viewnum");        return repository.findByDestId(destId, of);    }

评论:

评论类型:

微信朋友圈式评论:没有层次;

盖楼式评论:一层套着一层;

攻略的评论:

攻略评论表的设计:

评论点赞如何控制?什么时候控制显示什么颜色?
  1. 有用户的信息保留下来,记录谁点赞了;

  2. 评论里的点赞集合,用于存放给评论点赞的观众,评论里的集合中有存有观众,就将点赞的颜色变红;


添加评论:
    //在攻略下添加评论    @RequireLogin //需要登录才能操作    @PostMapping("/addComment")    public Object addComment(StrategyComment comment, @UserParam UserInfo userInfo) {        //设置评论里与用户相关的信息        BeanUtils.copyProperties(userInfo, comment);        comment.setUserId(userInfo.getId());//属性名字不同时,单独设置
//添加评论 strategyCommentService.addComment(comment);
//评论的分页 StrategyCommentQuery qo = new StrategyCommentQuery(); qo.setStrategyId(comment.getStrategyId()); Page<StrategyComment> page = strategyCommentService.query(qo);
//添加一条评论:redis中评论统计 + 1 strategyStatiesVORedisService.replynumIncrease(comment.getStrategyId(), 1);
//返回前台的数据 return JsonResult.success(new ParamMap() .put("page", page) .put("vo", strategyStatiesVORedisService.getStrategyStatisVo(comment.getStrategyId())) ); }
    //在攻略中添加评论    @Override    public void addComment(StrategyComment comment) {        //时间和id        comment.setCreateTime(new Date());        comment.setId(null);
//将comment保存到MongoDB repository.save(comment); }


点赞的操作实现:
  1. 必须登录之后才能点赞;

  2. 如果点赞成功,点赞数+1,然后颜色变红;

  3. 如果再点一次,取消点赞,点赞数-1,然后颜色变白;


关键:2、3如何实现

怎么去区分点赞还是取消点赞

在评论数据里设计一个用于存放点赞用户的list集合,用户发起请求时,先检查当前登录用户id是否在list中已经存在,如果存在,表示用户之前已经点赞过,此时就是取消点赞;

怎么实现

  1. 用户登录进来,先通过点赞用户的list集合判断当前登录用户id是否存在;

  2. 如果用户不存在list集合中,表示当前用户的请求是点赞请求,那么点赞数+1;同时将用户的id添加到list集合中

  3. 如果用户存在list集合中,表示当前用户的请求是取消点赞请求,那么点赞数-1;同时将用户的id从ist集合中移除


    //在攻略下的评论上点赞    @RequireLogin //需要登录才能操作    @PostMapping("/commentThumb")    public Object commentThumb(String cid, String sid, @UserParam UserInfo userInfo) {
//点赞操作 strategyCommentService.commentThumb(cid, sid);
//评论的点赞也要分页 StrategyCommentQuery qo = new StrategyCommentQuery(); qo.setStrategyId(sid); Page page = strategyCommentService.query(qo);
return JsonResult.success(page); }

commentThumb:

    //攻略里的评论点赞    @Override    public void commentThumb(String cid, String uid) {        //1.判断用户是点赞还是取消点赞        //获取存点赞用户的 id 集合 list        StrategyComment comment = this.get(cid);//评论是存在mongodb中的        List<String> userList = comment.getThumbuplist();//从评论中拿到存用户id的list
if (!userList.contains(uid)) { //2.不包含,点赞,list中存当前用户的id comment.setThumbupnum(comment.getThumbupnum() + 1); userList.add(uid);
}else { //3.包含,取消点赞,list中移除当前用户的id comment.setThumbupnum(comment.getThumbupnum() - 1); userList.remove(uid); } //4.将list改变的数据同步到mongodb中,更新 repository.save(comment); }

游记的评论:

游记评论表的设计:


评论添加:
    //游记评论    @PostMapping("/commentAdd")    @RequireLogin  //这个方法只能是登录的用户才能操作,所以用登录拦截器来限制未登录用户的操作    public Object commentAdd(TravelComment comment, @UserParam UserInfo userInfo) {
//用户游记评论 BeanUtils.copyProperties(userInfo, comment); comment.setUserId(userInfo.getId()); travelCommentService.addComment(comment);
//查询评论数据,一页显示 TravelCommentQuery qo = new TravelCommentQuery(); qo.setTravelId(comment.getTravelId()); qo.setPageSize(Integer.MAX_VALUE);//一页显示所有数据21亿条 Page<TravelComment> page = travelCommentService.query(qo);
return JsonResult.success(page); }
    //在攻略中添加评论    @Override    public void addComment(TravelComment comment) {
//查询评论数据 String refId = comment.getRefComment().getId(); if (StringUtils.hasLength(refId)) { //不是第一层 //设置引入评论 TravelComment refComment = this.get(refId); comment.setRefComment(refComment); } //保存进mongodb repository.save(comment); }


数据统计(使用Redis):

进入攻略明细页面,需要对viewnum阅读量进行+1,操作频繁,访问量大的时候对数据库的压力会很大?

问题:频繁的DML(DQL)操作

解决方案:减少DML(DQL)操作,缓存,让它操作缓存中存好的数据,而不是每次都操作到数据库,缓存会在一定时间段内(数据库压力不大时)将缓存中被操作的数据同步到数据库中。

局限性敏感数据(不允许丢失的:钱),不允许使用缓存的方式操作。因为缓存数据可能会丢失;

缓存框架

  1. map:JDK自带的,实现简单,但是操作麻烦,数据的缓存容易丢失

  2. ehcache:针对的是单体项目,可以实现数据缓存操作,分布式/微服务/集群支持较弱。

  3. redis:及支持单体也支持分布式/微服务/集群,如果项目是分布式或者微服务,首选Redis或者memcache,推荐Redis,因为Redis支持数据结构更多,能实现业务更加复杂;

  4. memcache


使用Redis的key-value如何设计:

第一种方式:添加或修改操作方便,但是页面获取麻烦(查不同的key5次)

第二种方式:封装起来获取的时候,用封装好的对象去拿不同的key更简单;


VO类的设计:
/** * 攻略redis中统计数据 * 运用模块: *  1:数据统计(回复,点赞,收藏,分享,查看) */@Getter@Setterpublic class StrategyStatisVO implements Serializable {
private String strategyId; //攻略id 在持久化的时候用到
private int viewnum; //点击数 private int replynum; //攻略评论数 private int favornum; //收藏数 private int sharenum; //分享数 private int thumbsupnum; //点赞个数}
浏览量:
//攻略阅读量的统计@Overridepublic void viewnumIncrease(String sid, int i) {    //1.判断vo对象是否存在    StrategyStatisVO vo = this.getStrategyStatisVo(sid);
//2.执行阅读数+1的操作 vo.setViewnum(vo.getViewnum() + i);
//将修改的数据更新 this.setStrategyStatisVo(vo);}

其中用到的方法:

//获取vo对象@Overridepublic StrategyStatisVO getStrategyStatisVo(String sid) {    //vo对象的key:strategy_statis_vo:sid    String key = RedisKeys.STRATEGY_STATIS_VO.join(sid);    StrategyStatisVO vo = null;    if (!template.hasKey(key)) {        //vo对象不存在Redis中,创建一个vo存进去        Strategy strategy = strategyService.get(sid);        vo = new StrategyStatisVO();        BeanUtils.copyProperties(strategy, vo);        vo.setStrategyId(sid);
//需要将初始化数据设置到Redis中 template.opsForValue().set(key, JSON.toJSONString(vo)); } else { //vo对象已经存在 String voStr = template.opsForValue().get(key); //解析成VO对象 vo = JSON.parseObject(voStr, StrategyStatisVO.class); } return vo;}

获取到vo之后,在统计方法中将阅读增量设置好,还要将修改的数据同步到Redis缓存中:

    //更改Redis的viewnum    @Override    public void setStrategyStatisVo(StrategyStatisVO vo) {        //更改        String key = RedisKeys.STRATEGY_STATIS_VO.join(vo.getStrategyId());        template.opsForValue().set(key, JSON.toJSONString(vo));    }
评论数量统计

在添加评论的方法addComment()中,添加一条评论,评论统计数量就+1;

    //统计评论数量    @Override    public void replynumIncrease(String strategyId, int i) {        //1.获取vo        StrategyStatisVO vo = this.getStrategyStatisVo(strategyId);
//2.评论数量 + 1 vo.setReplynum(vo.getReplynum() + 1);
//3.更新 this.setStrategyStatisVo(vo); }


收藏统计:

需求:用户攻略收藏

要求

  1. 用户必须要登录之后才可以进行收藏;

  2. 用户点击收藏时,收藏成功,收藏数+1,图标变蓝色;

  3. 用户点击取消收藏时,收藏数-1,图标变白色;

核心

用户点击发起的请求是收藏操作还是取消收藏操作;

  • 站在攻略角度:设计一个用户的id集合list;当当前用户发起了请求时,查看当前用户的id是否在用户id集合中,如果在,表示已经收藏过了,执行的是取消收藏,反之,执行收藏逻辑;

    • list的设计,还是使用Redis的方式:

  • 站在用户角度:设计一个攻略id集合list,当当前用户发起了请求时,查看当前攻略的id是否在攻略id的集合中,如果在,表示已经收藏过本篇攻略,执行取消收藏,反之,执行收藏逻辑;

两种方案哪种更合适:站在用户角度更合适。因为还有一个操作,用户查看自己收藏过的所有攻略时更方便。


实现逻辑

  1. 构建出一个用户角度的攻略id收藏集合list

  2. 请求进来时,先获取攻略id收藏集合list,判断集合中是否存在当前攻略id;

  3. 如果没有,表示是收藏请求,执行收藏逻辑,获取vo对象中的favornum + 1,往攻略id收藏集合list中加入当前攻略id;

  4. 如果有,表示是取消收藏请求,执行取消收藏逻辑,获取vo对象中的favornum - 1,将攻略id收藏集合list中当前攻略id移除

  5. 更新Redis里的攻略id收藏集合list,更新vo对象;


    //在攻略下的收藏    @RequireLogin //需要登录才能操作    @PostMapping("/favor")    public Object favor(String sid, @UserParam UserInfo userInfo) {
//攻略收藏:ret:true 收藏成功 ,false:取消收藏 boolean ret = strategyStatiesVORedisService.favor(sid, userInfo.getId());
List<String> sids = strategyStatiesVORedisService.getSids(userInfo.getId());
//返回ret结果给前台 return JsonResult.success(new ParamMap() .put("ret", ret) .put("sids",sids ) .put("vo", strategyStatiesVORedisService.getStrategyStatisVo(sid)) ); }

判断收藏还是取消收藏,就看存收藏攻略的集合中有没有存在了该攻略:

    //攻略收藏:ret:true 收藏成功 ,false:取消收藏    @Override    public boolean favor(String sid, String uid) {
//1.创建list专门存放用户收藏的攻略id //key:strategy_statis_vo:uid 站在用户的角度,list中存攻略id String listKey = RedisKeys.USER_STRATEGY_FAVOR.join(uid);
List<String> list = null; if (template.hasKey(listKey)) { //list在Redis中已经存在,就获取到它 String listStr = template.opsForValue().get(listKey); //将json字符串转换成list集合 参数2:list的泛型 list = JSON.parseArray(listStr, String.class); } else { //list在Redis中还不存在,创建并初始化 list = new ArrayList<>(); }
//判断攻略的id在list中是否已经存在 StrategyStatisVO vo = this.getStrategyStatisVo(sid); if (!list.contains(sid)) { //2.用户发起请求,查看list中是否已经存在了这个攻略id,不存在表示收藏 vo.setFavornum(vo.getFavornum() + 1); list.add(sid);//添加 } else { //3.用户发起请求,查看list中是否已经存在了这个攻略id,存在表示取消收藏 vo.setFavornum(vo.getFavornum() - 1); list.remove(sid);//移除 } //4.更新数据 template.opsForValue().set(listKey, JSON.toJSONString(list));//保存list this.setStrategyStatisVo(vo);//保存vo
return list.contains(sid);//一顿操作之后,最后sid在list中,表示收藏成功,true }

获取到用户收藏的攻略集合:

    @Override    public List<String> getSids(String uid) {        //收藏攻略list的key设计成前缀 + uid,此时用到了        String listKey = RedisKeys.USER_STRATEGY_FAVOR.join(uid);        //要拿vo中的list,得先确定有没有,在分情况处理        List<String> list = null;        if (template.hasKey(listKey)) {            //已经存在了            String listStr = template.opsForValue().get(listKey);            //将JSON格式的转换成List对象            list = JSON.parseArray(listStr, String.class);        } else {            //不存在创建            list = new ArrayList<>();        }        return list;    }
点赞(顶)统计:


需求:顶(点赞)操作

要求:

  1. 必须登陆之后才可以操作;

  2. 点赞时,判断是不是今天首次点赞,是提示点赞成功,点赞数 + 1;

  3. 一天只能点一次,多了就提示已经点赞过了;


核心:如何区分第一次顶和多次顶;

  1. 第一次点赞时,在Redis中做一个标记,表示已经点赞过了;

  2. 非第一次,就去Redis中看一下标记是否存在了,再做不同的操作;

  3. 一天只能顶一次:Redis中的标记是有时效性的,过了今天,就超时被删除了;


步骤

  1. 登录限制;

  2. 区分请求是第一次顶,还是多次顶;

  • key:不同的 用户 可以顶不同的 文章 ,但是每个文章只能顶一次,所以key要和用户(uid)、文章(sid)都关联起来

  • 时效时:存活时间计算:今天的最后一秒 - 当前时间  (即距今天结束还有多久就活多久)

  • 请求进来时,判断标记是否存在;

  • 如果不存在,表示今天可以点赞,点赞之后,将标记保存并且设置有效时间,顶数量 + 1,在Redis中更新数据;

  • 如果标记存在,表示今天已经顶过了,提示已经顶过了,其他不做任何操作;


  •     //攻略 顶点赞    @RequireLogin //需要登录才能操作    @PostMapping("/strategyThumbup")    public Object strategyThumbup(String sid, @UserParam UserInfo userInfo) {
    //攻略点赞:ret:true 点赞成功 ,false:今天已经点赞过 boolean ret = strategyStatiesVORedisService.strategyThumbup(sid, userInfo.getId());
    //返回ret结果给前台 return JsonResult.success(new ParamMap() .put("ret", ret) .put("vo", strategyStatiesVORedisService.getStrategyStatisVo(sid)) ); }
        //点赞攻略    @Override    public boolean strategyThumbup(String sid, String uid) {        //1.判断标记是否存在        String signKey = RedisKeys.STRATEGY_THUMBUP.join(uid, sid);//key和攻略用户都有关系,一个用户可以顶多个不同的,一个一天只能一次        if (!template.hasKey(signKey)) {            //2.如果不存在就将标记添加进vo中的顶,存在Redis中,点赞数 + 1,设置有效时间,更新vo对象            StrategyStatisVO vo = this.getStrategyStatisVo(sid);            vo.setThumbsupnum(vo.getThumbsupnum() + 1);            this.setStrategyStatisVo(vo);
    //设置时效性 Date now = new Date(); //`今天的最后一秒 - 当前时间` (即距今天结束还有多久就活多久) Date endDate = DateUtil.getEndDate(now); long dateBetween = DateUtil.getDateBetween(now, endDate); template.opsForValue().set(signKey, "1", dateBetween, TimeUnit.SECONDS); return true; } //3.存在就是点赞失败,一天之只能点一次 return false; }



    java学途

    只分享有用的Java技术资料 

    扫描二维码关注公众号

     


    笔记|学习资料|面试笔试题|经验分享 

    如有任何需求或问题欢迎骚扰。微信号:JL2020aini

    或扫描下方二维码添加小编微信

     




    小伙砸,欢迎再看分享给其他小伙伴!共同进步!




    本文分享自微信公众号 - java学途(javaxty)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    标签
    易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
    该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!