小Hub领读:

继续我们的eblog,今天来完成博客文章收藏,用户中心的设置!


项目名称:eblog

项目 Git 仓库:https://github.com/MarkerHub/eblog(给个 star 支持哈)

项目演示地址:https://markerhub.com:8082

前几篇项目讲解文章:

1、(eblog)Github 上最值得学习的 Springboot 开源博客项目!

2、(eblog)小 Hub 手把手教你如何从 0 搭建一个开源项目架构

3、(eblog)整合Redis,以及项目优雅的异常处理与返回结果封装

4、(eblog)用Redis的zset有序集合实现一个本周热议功能

5、(eblog)自定义Freemaker标签实现博客首页数据填充


1、细节调整

这一次作业,我们来修复一下bug,还有一些细节调整,因为博客的功能其实不多,业务逻辑也不复杂,后面我们还有搜索、群聊等功能,都是大模块。

文章收藏

文章收藏的js其实已经写好的了,只是有些条件没有触发而已,是什么条件呢,我们先来找到收藏的js先:

  • static/res/mods/jie.js

可以看到什么触发加载收藏的条件有两个:

  • 是否有id为LAY_jieAdmin的元素

  • layui.cache.user.uid是否为-1

LAY_jieAdmin是为了限定只有文章详情页才加载这个js,而其他页面不需要;那layui.cache.user.uid是哪里设置的呢?大家还记得我们一开始给html分模块的时候吗,我们在layout.ftl宏中有一段js,原本uid的值就是-1,所以我们需要把登录之后的值附上去。

  • templates/inc/layout.ftl

<script>layui.cache.page = 'jie';layui.cache.user = {username: '${profile.username!"游客"}',uid: ${profile.id!'-1'},avatar: '${profile.avatar!"/res/images/avatar/00.jpg"}',experience: 0,sex: '${profile.sex!'未知'}'};layui.config({version: "3.0.0",base: '/res/mods/'}).extend({fly: 'index'}).use('fly').use('jie').use('user');
</script>

熟系freemarker语法的同学应该都懂${profile.id!'-1'}是啥意思了,!后面表示当值为空的默认值。 好,改好了之后刷新一下,你会发现有个弹窗提示"请求异常,请重试",我们暂时先不管,先把收藏功能搞定先,看看是不是收藏功能controller还没有导致的。

从上图可以看出,我已经把查看是否收藏功能的链接改了一下

  • /collection/find/

功能代码其实很简单,就从UserCollection表中查询是否有记录就行了,如果有表明已经收藏了,js会渲染出取消收藏的按钮,如果没有记录,就会渲染收藏的按钮。

  • com.example.controller.PostController

@ResponseBody
@PostMapping("/collection/find/")
public Result collectionFind(Long cid) {int count = userCollectionService.count(new QueryWrapper<UserCollection>().eq("post_id", cid).eq("user_id", getProfileId()));return Result.succ(MapUtil.of("collection", count > 0));
}

根据js,我直接返回的是data中放一个参数collection是否为true就行了。渲染效果如下: 

然后点击按钮,发现有两个链接(我改了一下链接前缀):

  • /collection/add/

  • /collection/remove/

分别代表这收藏和取消收藏,所以我们分别写这两个controller,注意都是ajax请求来的。收藏的逻辑也比较简单,首先判断一下是否已经收藏过了,已经收藏就返回提示已经收藏,未收藏就添加一天记录即可。

  • com.example.controller.PostController

@ResponseBody
@PostMapping("/collection/add/")
public Result collectionAdd(Long cid) {Post post = postService.getById(cid);Assert.isTrue(post != null, "该帖子已被删除");int count = userCollectionService.count(new QueryWrapper<UserCollection>().eq("post_id", cid).eq("user_id", getProfileId()));if(count > 0) {return Result.fail("你已经收藏");}UserCollection collection = new UserCollection();collection.setUserId(getProfileId());collection.setCreated(new Date());collection.setModified(new Date());collection.setPostId(post.getId());collection.setPostUserId(post.getUserId());userCollectionService.save(collection);return Result.succ(MapUtil.of("collection", true));
}
  • com.example.controller.PostController

取消收藏的逻辑:删除一条记录即可

@ResponseBody
@PostMapping("/collection/remove/")
public Result collectionRemove(Long cid) {Post post = postService.getById(Long.valueOf(cid));Assert.isTrue(post != null, "该帖子已被删除");boolean hasRemove = userCollectionService.remove(new QueryWrapper<UserCollection>().eq("post_id", cid).eq("user_id", getProfileId()));return Result.succ(hasRemove);
}

ok,收藏设计到的3个方法已经开发完毕,点击文章详情页的收藏和取消收藏,都能正常执行代码!无bug~

消息未读

到了这时候,你发现,刷新页面之后,还是有弹窗提示,这是啥问题?浏览器打开F12,切换到Network标签,因为我们猜想应该是一些异步请求出了异常,所以触发了弹窗提示。接下来我们就要找到这个请求,Network下,我们再点击XHR,因为这表示是发起的异步请求的链接。从这里我们就看到了一个nums/的请求是404,具体的请求其实是:http://localhost:8080/message/nums/,

然后我们再全局搜索/message/nums找到发起这个异步请求的js地方:

所以我们确定了这个弹窗应该就是这引起的了。所以我们去写一下这个方法。这是新消息通知,我们之前在用户中心弄过一个我的消息,但是好像没有状态(已读和未读),所以我需要在UserMessage上添加一个status字段标识已读和未读。记得数据库要添加字段。

  • com.example.entity.UserMessage

/*** 状态:0未读,1已读*/
private Integer status;

然后我们再查下当前用户的状态未0的消息数量出来,就是新消息通知的数量了。

  • com.example.controller.IndexController

@ResponseBody
@PostMapping("/message/nums/")
public Object messageNums() throws IOException {int count = userMessageService.count(new QueryWrapper<UserMessage>().eq("to_user_id", getProfileId()).eq("status", 0));return MapUtil.builder().put("status", 0).put("count", count).build();
}

返回值是啥我是根据js推算出来的,js需要啥结果我就返回啥结果。重新运行代码之后,我们发现弹窗没有了,页面展示效果如下: 

消息通知

至此,新消息通知已经ok,接下来我们搞一个高大上一点的功能。我们刷微博简书头条等网站的时候,如果收到消息通知,一般来说不用我们刷新页面,而是实时给我们展示有消息来了,会突然有个新消息通知的图标提示我们,这是怎么做到的呢,结合我们之前学过的知识。我们可以找到几种方案来实现这个功能:

  • ajax定时加载刷新

  • websocket双工通讯

  • 长链接

我们课程中有节课是专门讲解websocket的,接下来我们就使用这个技术来实现这个功能。

同学们可以去回顾一下websocket的知识:

  • https://gitee.com/lv-success/git-third/tree/master/course-15-websocket/springboot-websocket

上面是一个springboot集成ws的demo,接下来我们安装这个例子的步骤把ws集成到我们现有的项目里面。

第一步:导入jar包

  • pom.xml

<!-- ws -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

第二步:编写ws配置

  • com.example.config.WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker//注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思。
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {/**** 注册 Stomp的端点* addEndpoint:添加STOMP协议的端点。提供WebSocket或SockJS客户端访问的地址* withSockJS:使用SockJS协议* @param registry*/public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/websocket").withSockJS();}/*** 配置消息代理* 启动Broker,消息的发送的地址符合配置的前缀来的消息才发送到这个broker*/public void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/user/", "/topic");//推送消息前缀registry.setApplicationDestinationPrefixes("/app");}
}

我们来解析一下这是啥意思,首先@EnableWebSocketMessageBroker,springboot的手动配置大家还记得吧?这个也是开启ws消息代理,然后继承WebSocketMessageBrokerConfigurer重写registerStompEndpoints和configureMessageBroker方法,registerStompEndpoints方法是注册端点,addEndpoint("/websocket")表示注册一个端点叫websocket,那么前端就能通过这个链接连接到服务器实现双工通讯了。.withSockJS()意思是使用SockJs协议,回顾一下:

  • SockJs是解决浏览器不支持ws的情况

  • Stompjs是简化文本传输的格式

configureMessageBroker是配置消息代理,上面我们配置了/user,/topic都是需要消息代理的链接。前端/app链接前缀过来的消息都会进入消息代理。

有了这两步骤后,我们就可以使用ws了,我们先来写一下前端:

因为我们的消息通知是在头部的用户名称那里,所有的页面都有,所以我们把js写在layout.ftl上。

$(function () {var elemUser = $('.fly-nav-user');if(layui.cache.user.uid !== -1 && elemUser[0]){var socket = new SockJS("/websocket");stompClient = Stomp.over(socket);stompClient.connect({},function (frame) {//subscribe订阅stompClient.subscribe('/user/' + ${profile.id} + '/messCount',function (res) {showTips(res.body);})})}
});

前面的if判断,我是根据收藏那里来写的,var socket = new SockJS("/websocket");表示建立端点链接,这样前端就会和后端建立ws双工通道,stompClient = Stomp.over(socket);表示切换成stomp文本传输协议传输内容。stompClient.connect表示建立连接触发的方法,这个方法里面有个stompClient.subscribe,差不多就表示订阅这个消息队列的意思,当后端往/user/{userId}/messCount里面发送消息时候,当前用户就能接收到消息,res.body就是返回的内容,然后就是showTips方法,这个方法其实就是渲染新消息通知的样式,我们从之前的新消息通知那里吧对应的js复制过来即可:

function showTips(count) {var msg = $('<a class="fly-nav-msg" href="javascript:;">'+ count +'</a>');var elemUser = $('.fly-nav-user');elemUser.append(msg);msg.on('click', function(){location.href = '/center/message/';});layer.tips('你有 '+ count +' 条未读消息', msg, {tips: 3,tipsMore: true,fixed: true});msg.on('mouseenter', function(){layer.closeAll('tips');})
}

ok,那前端我们已经可以连上ws实现双工通讯,并且监听了/user/{userId}/messCount这个队列,所以后端往这里面发送消息前端就能收到然后实现showTips方法。 那后端什么时候该发送消息给前端呢?

  • 有人评论了作者文章,或者回复作者的评论

  • 系统消息等

ok,我们先来写一个wsService,写一个发送消息数量给前端的方法。

  • com.example.service.WsService

void sendMessCountToUser(Long userId, Integer count);

他的实现类复杂嘛?其实不复杂,我们先来看下参数,userId,就是限定要给谁发送消息,count是消息数量,这里我们考虑多种情况,但count不为空时候,我们返回count数量的,当count为空时候,我们搜索userId所有未读的消息数量然后返回。

  • com.example.service.impl.WsServiceImpl

@Slf4j
@Service
public class WsServiceImpl implements WsService {@Autowiredprivate SimpMessagingTemplate messagingTemplate ;@AutowiredUserMessageService userMessageService;/*** 订阅链接为/user/{userId}/messCount的用户能收到消息* /user为默认前缀** @param userId* @param count*/@Asyncpublic void sendMessCountToUser(Long userId, Integer count) {if(count == null) {count = userMessageService.count(new QueryWrapper<UserMessage>().eq("status", 0).eq("to_user_id", userId));}this.messagingTemplate.convertAndSendToUser(userId.toString(),"/messCount", count);log.info("ws发送消息成功------------> {}, 数量:{}", userId, count);}
}

发送ws消息,用的是SimpMessagingTemplate,convertAndSendToUser方法会自动在前面添加前缀/user,然后是userId,加上后面的后缀/messCount,所以加起来的链接其实就是/user/{userId}/messCount,那么我们在需要发送消息的地方调用这个方法即可。 然后这里还有个内容要点,就是这里我用了一个@Async表示异步,从线程角度来说就是新起一个线程来执行这个方法,从而保证不影响调用方的事务和执行时间等。

那么我们来说下@Async的用法

异步@Async

其实这里我原本是想用队列来实现的,也能表示异步。本着让同学们接触到更多知识,我们这里就用了@Aysnc注解来实现,后面我们还是会用到MQ的,同学们别急。

使用这个注解我们需要开启异步配置。注解是@EnableAsync

  • com.example.config.AsyncConfig

@EnableAsync
@Configuration
public class AsyncConfig {@BeanAsyncTaskExecutor asyncTaskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(100);executor.setQueueCapacity(25);executor.setMaxPoolSize(500);return executor;}
}

所以使用了@EnableAsync注解之后我们就可以使用@Aysnc注解来实现异步了,asyncTaskExecutor()其实就是我用来重写AsyncTaskExecutor用的,定义了最大线程组等信息。另外Async其实还可以配置很多信息,比如异步线程出错时候的处理(重试等),大家课后可以多查询一下资料,这注解我在工作中运用其实还比较多的。 好了,上面我们已经把发送ws消息的方法改成了异步方法,会起一个线程执行发送。我们现在需要调用的地方其实就是在评论那里。

  • com.example.controller.PostController#reply(Long, Long, String)

这样有人评论文章或者回复评论的时候,都能实时收到消息了,我们来演示一下效果:

至此,我们实现了传说中的实时通知功能!要膨胀了~

文章阅读量

接下来的任务,我们是要完善一下文章阅读量。之前访问文章,阅读量都没增加,现在我们来补上 这个一个bug。怎么做呢?是每访问一次我们就直接修改数据库?这里我们使用缓存在解决这个问题,每次访问,我们就直接缓存的阅读量增一,然后在某一时刻再同步到数据库中即可。访问文章时候,我们把缓存中的阅读量传到vo中,具体咋样的呢,我们找到之前写的com.example.controller.PostController#view方法,然后我加了这一句代码:

技术要把vo的viewCount值修改成缓存的数量。

  • com.example.service.impl.PostServiceImpl#setViewCount

@Override
public void setViewCount(Post post) {// 从缓存中获取阅读数量Integer viewCount = (Integer) redisUtil.hget("rank_post_" + post.getId(), "post:viewCount");if(viewCount != null) {post.setViewCount((Integer) viewCount + 1);} else {post.setViewCount(post.getViewCount() + 1);}// 设置新的阅读redisUtil.hset("rank_post_" + post.getId(), "post:viewCount", post.getViewCount());
}

从代码中可以看到,我们先从缓存中获取ViewCount,然后设置post.setViewCount,最后再把加一之后的值同步到redis中。 ok,这一步还是比较简单的,接下来我们要起一个定时器,然后定时吧缓存中的阅读量同步到数据库中,实现数据同步。

  • com.example.schedules.ScheduledTasks

@Slf4j
@Component
public class ScheduledTasks {@AutowiredRedisUtil redisUtil;@Autowiredprivate RedisTemplate redisTemplate;@AutowiredPostService postService;/*** 阅读数量同步任务* 每天2点同步*/
//    @Scheduled(cron = "0 0 2 * * ?")@Scheduled(cron = "0 0/1 * * * *")//一分钟(测试用)public void postViewCountSync() {Set<String> keys = redisTemplate.keys("rank_post_*");List<String> ids = new ArrayList<>();for (String key : keys) {String postId = key.substring("rank_post_".length());if(redisUtil.hHasKey("rank_post_" + postId, "post:viewCount")){ids.add(postId);}}if(ids.isEmpty()) return;List<Post> posts = postService.list(new QueryWrapper<Post>().in("id", ids));Iterator<Post> it = posts.iterator();List<String> syncKeys = new ArrayList<>();while (it.hasNext()) {Post post = it.next();Object count =redisUtil.hget("rank_post_" + post.getId(), "post:viewCount");if(count != null) {post.setViewCount(Integer.valueOf(count.toString()));syncKeys.add("rank_post_" + post.getId());} else {//不需要同步的}}if(posts.isEmpty()) return;boolean isSuccess = postService.updateBatchById(posts);if(isSuccess) {for(Post post : posts) {// 删除缓存中的阅读数量,防止重复同步(根据实际情况来)redisUtil.hdel("rank_post_" + post.getId(), "post:viewCount");}}log.info("同步文章阅读成功 ------> {}", syncKeys);}
}

为何获取所有需要同步阅读的列表,我们用了keys命令,实际上当redis的缓存越来越大的时候,我们是不能再使用这keys命令的,因为keys命令会检索所有的key,是个耗时的过程,而redis又是个单线程的中间件,会影响其他命令的执行。所以理论上我们需要用scan命令。考虑到这里博客只是个简单业务,redis不会很大,所以就直接用了keys命令,后期大家可以自行优化。 然后获取到列表后,然后就是获取所有的实体,然后批量更新阅读量。

 给eblog一个star,感谢支持哈

(eblog)8、消息异步通知、细节调整相关推荐

  1. Java支付宝APP支付-验证异步通知消息

    上一章已经讲述了支付宝如何生成支付订单,这一章讲述一下支付宝生成订单之后,异步通知接口的开发. 这里先讲一下啥叫支付宝异步通知:对于App支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址 ...

  2. Oracle Advanced Queuing 触发器入列和异步通知消息出列

    1. 官方文档: https://docs.oracle.com/database/121/ADQUE/aq_opers.htm 2. 授权用户操作dbms_aq/dbms_aqadm 的权限 gra ...

  3. 最好用的 6 款 Vue 实时消息提示通知(Message/Notification)组件推荐与测评

    本文完整版:<最好用的 6 款 Vue 实时消息提示通知(Message/Notification)组件推荐与测评> Vue 实时消息提示通知 Vue-notification - 专注实 ...

  4. 网易视频云专注于技术服务——异步通知系统

    随着互联网技术的发展,云计算在资源层面竞争已经进入白热化的阶段,网易视频云定位于PAAS层面,从各个技术领域进行技术钻研以提供给用户更高效.更稳定.更便捷的视频直播技术服务:notify系统作为网易视 ...

  5. 魔坊APP项目-25-种植园,植物的状态改动、当果树种植以后在celery的异步任务中调整浇水的状态、客户端通过倒计时判断时间,显示浇水道具

    种植园 植物的状态改动 一.当果树种植以后在celery的异步任务中调整浇水的状态 在进行果树种植的时候, 在服务端设置当前果树到等待浇水的redis变量中.通过celery不断进行周期任务的处理, ...

  6. 魔坊APP项目-18-种植园,基于支付宝提供的沙箱测试环境开发支付接口、服务端, 处理支付结果的同步通知和异步通知、修复页面底部菜单无法被点击的BUG

    种植园 一.基于支付宝提供的沙箱测试环境开发支付接口 沙箱环境: https://openhome.alipay.com/platform/appDaily.htm?tab=info 开发文档: ht ...

  7. 支付宝接口使用文档说明 支付宝异步通知

    支付宝接口使用文档说明 支付宝异步通知(notify_url)与return_url. 现支付宝的通知有两类.  A服务器通知,对应的参数为notify_url,支付宝通知使用POST方式  B页面跳 ...

  8. 第三十四篇:在SOUI中使用异步通知

    概述 异步通知是客户端开发中常见的需求,比如在一个网络处理线程中要通知UI线程更新等等. 通常在Windows编程中,为了方便,我们一般会向UI线程的窗口句柄Post/Send一个窗口消息从而达到将非 ...

  9. 支付宝生活号异步通知地址_异步生活。

    支付宝生活号异步通知地址 Last year I turned off all my notifications. I stopped booking meetings. I started livi ...

最新文章

  1. day1作业(格式化输出)
  2. Spring Boot 第三篇:SpringBoot用JdbcTemplates访问Mysql
  3. 开放式创新改变世界——OpenStack生态系统将重新洗牌
  4. 大规模集群中Docker镜像如何分发管理?试试Uber刚开源的Kraken
  5. R语言 非中心化F分布
  6. 二十九、电子商务服务推荐项目基本描述
  7. thinkphp路由的作用
  8. 详细讲述STP过程【转自56cto.com】
  9. java asm 中文文档_Java ASM3学习(3)
  10. react脚手架搭建项目目录介绍
  11. SQL Server基本函数
  12. win7开机有画面进系统黑屏怎么办
  13. SN3FAP反激式开关电源12V2A输出电路
  14. python全角数字_python 半角全角的相互转换
  15. 读javascript高级程序设计09-BOM
  16. 从普通温度表到高精度测量
  17. 新东方老师谈如何学英语
  18. .NET使用MailKit进行邮件处理
  19. ESP32 关于HTTPS的使用
  20. HTML+CSS实战提升

热门文章

  1. 1044 mysql_MYSQL 有ERROR 1044
  2. 大数据学前准备--zookeeper详解与集群搭建(保姆级教程)
  3. java cda安装_JAVA如何与R完美结合起来
  4. 教你用认知和人性来做最棒的程序员
  5. 比较好的开源商城系统
  6. 两个坐标系转换的变换矩阵
  7. 【Active Learning - 13】总结与展望 参考文献的整理与分享(The End...)
  8. JVM中垃圾收集算法总结
  9. Yo(Yeoman)
  10. B2B2C多用户商城系统如何选择