游记:
游记是用户自己发表的。由用户自己管理,管理人员只负责审核和发布;
需求:查看和拒绝
用户的文章由前端用户自己维护,管理仅仅显示和对状态进行管理,不能进行添加编辑操作;
查看是前台只需要游记的内容即可,后台将游记的内容反给前台。根据当前游记的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 = 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;//目的地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:
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);
}
封面的图片上传:
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) {
//保存修改操作只能登录后才能操作,需要在表现层验证用户是否登录
//获得当前登录的用户信息,通过令牌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自定义注解用来去分参数使用自定义注解解析器;详细在后面。
//自定义参数解析器测试
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 —— 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
流程:
页面请求进入到请求映射方法时,此时的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查询阅读量前三的攻略
@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);
}
评论:
评论类型:
微信朋友圈式评论:没有层次;
盖楼式评论:一层套着一层;
攻略的评论:
攻略评论表的设计:
评论点赞如何控制?什么时候控制显示什么颜色?
有用户的信息保留下来,记录谁点赞了;
评论里的点赞集合,用于存放给评论点赞的观众,评论里的集合中有存有观众,就将点赞的颜色变红;
添加评论:
//在攻略下添加评论
//需要登录才能操作
"/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中评论统计 + 1
strategyStatiesVORedisService.replynumIncrease(comment.getStrategyId(), 1);
//返回前台的数据
return JsonResult.success(new ParamMap()
.put("page", page)
.put("vo", strategyStatiesVORedisService.getStrategyStatisVo(comment.getStrategyId()))
);
}
//在攻略中添加评论
public void addComment(StrategyComment comment) {
//时间和id
comment.setCreateTime(new Date());
comment.setId(null);
//将comment保存到MongoDB
repository.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:
//攻略里的评论点赞
@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);
}
游记的评论:
游记评论表的设计:
评论添加:
//游记评论
"/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);
}
//在攻略中添加评论
@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)操作,缓存,让它操作缓存中存好的数据,而不是每次都操作到数据库,缓存会在一定时间段内(数据库压力不大时)将缓存中被操作的数据同步到数据库中。
局限性:敏感数据(不允许丢失的:钱),不允许使用缓存的方式操作。因为缓存数据可能会丢失;
缓存框架:
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: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;
//统计评论数量
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,图标变蓝色;
用户点击取消收藏时,收藏数-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:取消收藏
@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
}
获取到用户收藏的攻略集合:
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