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

需求:查看和拒绝

用户的文章由前端用户自己维护,管理仅仅显示和对状态进行管理,不能进行添加编辑操作;
查看是前台只需要游记的内容即可,后台将游记的内容反给前台。根据当前游记的id查到游记对象,在从游记对象中getContent()返回即可;
审核状态:
审核逻辑:游记满足什么条件才进行审核,审核通过和审核不通过分别需要做什么操作?
审核通过是发布状态;
只对状态是待审核的游记才进行审核;
审核通过/拒绝将游记的状态改成审核通过/审核拒绝;
审核通过之后还需要改变游记的发布时间和最后修改时间;
用户修改内容之后,需要考虑那些统计数据(点赞阅读等)要不要清空(问你的产品经理);
//审核游记@Overridepublic 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 stateTravel 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 = 4和max = 7,之后才能使用最大最小值来拼接出查询语句。
思考:
用
Map的K-V对来做映射关系。但是:map中的value不能放范围!用对象来对范围进行封装;

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

要将传过来的条件参数封装到query中;
封装范围条件的类:
/*** 游记范围查询条件:封装键值对的映射关系*/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;}}
游记高级查询条件:
// 游记高级分页查条件public class TravelQuery extends QueryObject {private String destId;//目的地idprivate 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:
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.tvif (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);}
封面的图片上传:
public class UploadController {//百度富文本编辑器的图片上传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中处理验证用户是否登录。让它们层次分明:
//保存或者修改("/saveOrUpdate")//这个方法只能是登录的用户才能操作,所以用登录拦截器来限制未登录用户的操作public Object saveOrUpdate(Travel travel, HttpServletRequest request) {//保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录//获得当前登录的用户信息,通过令牌tokenString token = request.getHeader("token");//判断用户是否登录UserInfo user = userInfoRedisService.getUserInfoByToken(token);if (user != null) {travel.setUserId(user.getId());}//最新刚刚保存/修改的游记的idString id = travelService.saveOrUpdate(travel);return JsonResult.success(id);}
//保存或者修改游记@Overridepublic 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来获取用户信息;简单办法:通过参数注入的方式实现用户对象的注入:
之前判断用户登录的在做发:
//保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录//获得当前登录的用户信息,通过令牌tokenString token = request.getHeader("token");//判断用户是否登录UserInfo user = userInfoRedisService.getUserInfoByToken(token);if (user != null) {travel.setUserId(user.getId());}
我希望或取用户信息遍的这样简单:
直接通过controller方法的形式参数直接得到用户的信息。这样前台需要什么信息我们都可以通过userInfo.getXxx()了。
@UserParam自定义注解用来去分参数使用自定义注解解析器;详细在后面。
//自定义参数解析器测试public Object info( UserInfo userInfo) {System.err.println(userInfo);return JsonResult.success(userInfo);}
怎么实现:
需要使用springmvc参数解析器,(以前的用户参数解析器:文件上传参数解析器),springmvc没有提供当前用户的参数解析器,需要我们自定义;
自定义参数解析器,在请求方法的参数中自动注入当前登录用户;
自定义参数解析器:
作用:能将请求映射方法的形式参数中的UserInfo对象转换成当前登录用户对象;
前提:
继承HandlerMethodArgumentResolver类,实现两个方法supportParameter() resolveArgument();
/*** 自定义参数解析器:在请求方法的参数中自动注入当前登录用户*/public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {private IUserInfoRedisService userInfoRedisService;//表示该参数解析器支持什么类型的参数,这里支持UserInfo类型的参数//如果该方法返回true,表示UserInfoArgumentResolver支持对UserInfo类型的参数进行解析public boolean supportsParameter(MethodParameter methodParameter) {return methodParameter.getParameterType() == UserInfo.class;}//执行解析逻辑,请求映射方法的形参UserInfo对象转换成当前用户的登录对象//上面的方法返回true,才会执行这一个方法public Object resolveArgument(MethodParameter methodParameter,ModelAndViewContainer modelAndViewContainer,NativeWebRequest nativeWebRequest,WebDataBinderFactory webDataBinderFactory) throws Exception {//请求头 —— token —— userInfoHttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);//通过反射拿到请求对象String token = request.getHeader("token");//通过令牌拿到当前登录用户信息return userInfoRedisService.getUserInfoByToken(token);}}
使用参数解析器:
在配置启动类中配置用户的参数解析器:
//用户的参数解析器@Beanpublic 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
流程:
页面请求进入到请求映射方法时,此时的springmvc会调用所有参数解析器(包括自定义的参数解析器)对请求方法中的形式参数尝试进行解析;
所有解析器遍历执行suppertsPramenter方法,如果某个参数类型使解析器的suppertsPramenter方法返回true,name终止遍历,使用该解析器的resolveArgument方法对参数进行解析;
此时:遍历到了自定义参数解析器,UserInfoResolver的suppertsPramenter方法返回true,表示这个解析器支持解析UserInfo类型的参数;
springmvc使用这个解析器来实现这个参数的解析,具体的实现就是让UserInfoResolver的对象resolveArgument,将返回值注入到请求方法中的UserInfo对象中。
定义注解标记:
问题:springmvc中有默认的参数解析器,怎么区分让它用自定义的参数解析器,还是默认的参数解析器:
标记区分:注解
/*** 用户参数的注解,用于标记自定义注解解析UserInfo类型的controller形式参数*///贴在方法的形式参数上//有效期:运行时public UserParam {}
修改参数解析器的解析条件:
//表示该参数解析器支持什么类型的参数,这里支持UserInfo类型的参数//如果该方法返回true,表示UserInfoArgumentResolver支持对UserInfo类型的参数进行解析public boolean supportsParameter(MethodParameter methodParameter) {//条件:类型为UserInfo并且带有@UserParam注解的参数才会解析return methodParameter.getParameterType() == UserInfo.class&& methodParameter.hasMethodAnnotation(UserParam.class);}
游记详情:

关联目的地
查询当前篇游记的目的地, 关联查询目的地的父目的地, 显示当前目的地的封面与名称
关联的攻略
根据当前篇游记的目的地查询该目的地下阅读量为前3的攻略,循环播放
关联的游记
根据当前篇游记的目的地查询该目的地下阅读量为前3的游记,循环播放
游记评论
这里评论采用的是一级评论方式显示, 但可以使用引用方式显示上一级评论
controller :
//游记详情("/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查询阅读量前三的攻略@Overridepublic List<Strategy> getViewnumTop3(String destId) {//点击量前三PageRequest of = PageRequest.of(0, 3, Sort.Direction.DESC, "viewnum");return repository.findByDestId(destId, of);}
travelService.getViewnumTop3方法:
//通过目的地id查阅读量前三的游记@Overridepublic List<Travel> getViewnumTop3(String destId) {PageRequest of = PageRequest.of(0, 3, Sort.Direction.DESC, "viewnum");return repository.findByDestId(destId, of);}
评论:
评论类型:
微信朋友圈式评论:没有层次;
盖楼式评论:一层套着一层;
攻略的评论:

攻略评论表的设计:

评论点赞如何控制?什么时候控制显示什么颜色?
有用户的信息保留下来,记录谁点赞了;
评论里的点赞集合,用于存放给评论点赞的观众,评论里的集合中有存有观众,就将点赞的颜色变红;
添加评论:
//在攻略下添加评论//需要登录才能操作("/addComment")public Object addComment(StrategyComment comment, 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中评论统计 + 1strategyStatiesVORedisService.replynumIncrease(comment.getStrategyId(), 1);//返回前台的数据return JsonResult.success(new ParamMap().put("page", page).put("vo", strategyStatiesVORedisService.getStrategyStatisVo(comment.getStrategyId())));}
//在攻略中添加评论public void addComment(StrategyComment comment) {//时间和idcomment.setCreateTime(new Date());comment.setId(null);//将comment保存到MongoDBrepository.save(comment);}
点赞的操作实现:
必须登录之后才能点赞;
如果点赞成功,点赞数+1,然后颜色变红;
如果再点一次,取消点赞,点赞数-1,然后颜色变白;
关键:2、3如何实现
怎么去区分点赞还是取消点赞;
在评论数据里设计一个用于存放点赞用户的list集合,用户发起请求时,先检查当前登录用户id是否在list中已经存在,如果存在,表示用户之前已经点赞过,此时就是取消点赞;
怎么实现:
用户登录进来,先通过点赞用户的list集合判断当前登录用户id是否存在;
如果用户不存在list集合中,表示当前用户的请求是点赞请求,那么点赞数+1;同时将用户的id添加到list集合中;
如果用户存在list集合中,表示当前用户的请求是取消点赞请求,那么点赞数-1;同时将用户的id从ist集合中移除;
//在攻略下的评论上点赞//需要登录才能操作("/commentThumb")public Object commentThumb(String cid, String sid, UserInfo userInfo) {//点赞操作strategyCommentService.commentThumb(cid, sid);//评论的点赞也要分页StrategyCommentQuery qo = new StrategyCommentQuery();qo.setStrategyId(sid);Page page = strategyCommentService.query(qo);return JsonResult.success(page);}
commentThumb:
//攻略里的评论点赞@Overridepublic void commentThumb(String cid, String uid) {//1.判断用户是点赞还是取消点赞//获取存点赞用户的 id 集合 listStrategyComment comment = this.get(cid);//评论是存在mongodb中的List<String> userList = comment.getThumbuplist();//从评论中拿到存用户id的listif (!userList.contains(uid)) {//2.不包含,点赞,list中存当前用户的idcomment.setThumbupnum(comment.getThumbupnum() + 1);userList.add(uid);}else {//3.包含,取消点赞,list中移除当前用户的idcomment.setThumbupnum(comment.getThumbupnum() - 1);userList.remove(uid);}//4.将list改变的数据同步到mongodb中,更新repository.save(comment);}
游记的评论:

游记评论表的设计:

评论添加:
//游记评论("/commentAdd")//这个方法只能是登录的用户才能操作,所以用登录拦截器来限制未登录用户的操作public Object commentAdd(TravelComment comment, 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);}
//在攻略中添加评论@Overridepublic void addComment(TravelComment comment) {//查询评论数据String refId = comment.getRefComment().getId();if (StringUtils.hasLength(refId)) {//不是第一层//设置引入评论TravelComment refComment = this.get(refId);comment.setRefComment(refComment);}//保存进mongodbrepository.save(comment);}
数据统计(使用Redis):

进入攻略明细页面,需要对viewnum阅读量进行+1,操作频繁,访问量大的时候对数据库的压力会很大?
问题:频繁的DML(DQL)操作
解决方案:减少DML(DQL)操作,缓存,让它操作缓存中存好的数据,而不是每次都操作到数据库,缓存会在一定时间段内(数据库压力不大时)将缓存中被操作的数据同步到数据库中。
局限性:敏感数据(不允许丢失的:钱),不允许使用缓存的方式操作。因为缓存数据可能会丢失;
缓存框架:
map:JDK自带的,实现简单,但是操作麻烦,数据的缓存容易丢失
ehcache:针对的是单体项目,可以实现数据缓存操作,分布式/微服务/集群支持较弱。
redis:及支持单体也支持分布式/微服务/集群,如果项目是分布式或者微服务,首选Redis或者memcache,推荐Redis,因为Redis支持数据结构更多,能实现业务更加复杂;
memcache
使用Redis的key-value如何设计:

第一种方式:添加或修改操作方便,但是页面获取麻烦(查不同的key5次)
第二种方式:封装起来获取的时候,用封装好的对象去拿不同的key更简单;
VO类的设计:
/*** 攻略redis中统计数据* 运用模块:* 1:数据统计(回复,点赞,收藏,分享,查看)*/public class StrategyStatisVO implements Serializable {private String strategyId; //攻略id 在持久化的时候用到private int viewnum; //点击数private int replynum; //攻略评论数private int favornum; //收藏数private int sharenum; //分享数private int thumbsupnum; //点赞个数}
浏览量:
//攻略阅读量的统计public void viewnumIncrease(String sid, int i) {//1.判断vo对象是否存在StrategyStatisVO vo = this.getStrategyStatisVo(sid);//2.执行阅读数+1的操作vo.setViewnum(vo.getViewnum() + i);//将修改的数据更新this.setStrategyStatisVo(vo);}
其中用到的方法:
//获取vo对象public StrategyStatisVO getStrategyStatisVo(String sid) {//vo对象的key:strategy_statis_vo:sidString 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@Overridepublic void setStrategyStatisVo(StrategyStatisVO vo) {//更改String key = RedisKeys.STRATEGY_STATIS_VO.join(vo.getStrategyId());template.opsForValue().set(key, JSON.toJSONString(vo));}
评论数量统计
在添加评论的方法addComment()中,添加一条评论,评论统计数量就+1;
//统计评论数量public void replynumIncrease(String strategyId, int i) {//1.获取voStrategyStatisVO vo = this.getStrategyStatisVo(strategyId);//2.评论数量 + 1vo.setReplynum(vo.getReplynum() + 1);//3.更新this.setStrategyStatisVo(vo);}
收藏统计:

需求:用户攻略收藏
要求:
用户必须要登录之后才可以进行收藏;
用户点击收藏时,收藏成功,收藏数+1,图标变蓝色;
用户点击取消收藏时,收藏数-1,图标变白色;
核心:
用户点击发起的请求是收藏操作还是取消收藏操作;
站在攻略角度:设计一个用户的id集合list;当当前用户发起了请求时,查看当前用户的id是否在用户id集合中,如果在,表示已经收藏过了,执行的是取消收藏,反之,执行收藏逻辑;
list的设计,还是使用Redis的方式:

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

两种方案哪种更合适:站在用户角度更合适。因为还有一个操作,用户查看自己收藏过的所有攻略时更方便。
实现逻辑:
构建出一个用户角度的攻略id收藏集合list

请求进来时,先获取攻略id收藏集合list,判断集合中是否存在当前攻略id;
如果没有,表示是收藏请求,执行收藏逻辑,获取vo对象中的favornum + 1,往攻略id收藏集合list中加入当前攻略id;
如果有,表示是取消收藏请求,执行取消收藏逻辑,获取vo对象中的favornum - 1,将攻略id收藏集合list中当前攻略id移除;
更新Redis里的攻略id收藏集合list,更新vo对象;
//在攻略下的收藏//需要登录才能操作("/favor")public Object favor(String sid, 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:取消收藏@Overridepublic boolean favor(String sid, String uid) {//1.创建list专门存放用户收藏的攻略id//key:strategy_statis_vo:uid 站在用户的角度,list中存攻略idString 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));//保存listthis.setStrategyStatisVo(vo);//保存voreturn list.contains(sid);//一顿操作之后,最后sid在list中,表示收藏成功,true}
获取到用户收藏的攻略集合:
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;
一天只能点一次,多了就提示已经点赞过了;
核心:如何区分第一次顶和多次顶;
第一次点赞时,在Redis中做一个标记,表示已经点赞过了;
非第一次,就去Redis中看一下标记是否存在了,再做不同的操作;
一天只能顶一次:Redis中的标记是有时效性的,过了今天,就超时被删除了;
步骤:
登录限制;
区分请求是第一次顶,还是多次顶;

key:不同的 用户 可以顶不同的 文章 ,但是每个文章只能顶一次,所以key要和用户(uid)、文章(sid)都关联起来;
时效时:存活时间计算:
今天的最后一秒 - 当前时间(即距今天结束还有多久就活多久)
请求进来时,判断标记是否存在;
如果不存在,表示今天可以点赞,点赞之后,将标记保存并且设置有效时间,顶数量 + 1,在Redis中更新数据;
如果标记存在,表示今天已经顶过了,提示已经顶过了,其他不做任何操作;
//攻略 顶点赞//需要登录才能操作("/strategyThumbup")public Object strategyThumbup(String sid, 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)));}
//点赞攻略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学途(javaxty)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4673060/blog/4680831




