日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

(eblog)8、消息异步通知、细节调整

發(fā)布時間:2023/12/20 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 (eblog)8、消息异步通知、细节调整 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

小Hub領(lǐng)讀:

繼續(xù)我們的eblog,今天來完成博客文章收藏,用戶中心的設(shè)置!


項目名稱:eblog

項目 Git 倉庫:https://github.com/MarkerHub/eblog(給個 star 支持哈)

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

前幾篇項目講解文章:

1、(eblog)Github 上最值得學(xué)習(xí)的 Springboot 開源博客項目!

2、(eblog)小 Hub 手把手教你如何從 0 搭建一個開源項目架構(gòu)

3、(eblog)整合Redis,以及項目優(yōu)雅的異常處理與返回結(jié)果封裝

4、(eblog)用Redis的zset有序集合實現(xiàn)一個本周熱議功能

5、(eblog)自定義Freemaker標(biāo)簽實現(xiàn)博客首頁數(shù)據(jù)填充


1、細(xì)節(jié)調(diào)整

這一次作業(yè),我們來修復(fù)一下bug,還有一些細(xì)節(jié)調(diào)整,因為博客的功能其實不多,業(yè)務(wù)邏輯也不復(fù)雜,后面我們還有搜索、群聊等功能,都是大模塊。

文章收藏

文章收藏的js其實已經(jīng)寫好的了,只是有些條件沒有觸發(fā)而已,是什么條件呢,我們先來找到收藏的js先:

  • static/res/mods/jie.js

可以看到什么觸發(fā)加載收藏的條件有兩個:

  • 是否有id為LAY_jieAdmin的元素

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

LAY_jieAdmin是為了限定只有文章詳情頁才加載這個js,而其他頁面不需要;那layui.cache.user.uid是哪里設(shè)置的呢?大家還記得我們一開始給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語法的同學(xué)應(yīng)該都懂${profile.id!'-1'}是啥意思了,!后面表示當(dāng)值為空的默認(rèn)值。 好,改好了之后刷新一下,你會發(fā)現(xiàn)有個彈窗提示"請求異常,請重試",我們暫時先不管,先把收藏功能搞定先,看看是不是收藏功能controller還沒有導(dǎo)致的。

從上圖可以看出,我已經(jīng)把查看是否收藏功能的鏈接改了一下

  • /collection/find/

功能代碼其實很簡單,就從UserCollection表中查詢是否有記錄就行了,如果有表明已經(jīng)收藏了,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)); }

根據(jù)js,我直接返回的是data中放一個參數(shù)collection是否為true就行了。渲染效果如下:?

然后點擊按鈕,發(fā)現(xiàn)有兩個鏈接(我改了一下鏈接前綴):

  • /collection/add/

  • /collection/remove/

分別代表這收藏和取消收藏,所以我們分別寫這兩個controller,注意都是ajax請求來的。收藏的邏輯也比較簡單,首先判斷一下是否已經(jīng)收藏過了,已經(jīng)收藏就返回提示已經(jīng)收藏,未收藏就添加一天記錄即可。

  • 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("你已經(jīng)收藏");}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,收藏設(shè)計到的3個方法已經(jīng)開發(fā)完畢,點擊文章詳情頁的收藏和取消收藏,都能正常執(zhí)行代碼!無bug~

消息未讀

到了這時候,你發(fā)現(xiàn),刷新頁面之后,還是有彈窗提示,這是啥問題?瀏覽器打開F12,切換到Network標(biāo)簽,因為我們猜想應(yīng)該是一些異步請求出了異常,所以觸發(fā)了彈窗提示。接下來我們就要找到這個請求,Network下,我們再點擊XHR,因為這表示是發(fā)起的異步請求的鏈接。從這里我們就看到了一個nums/的請求是404,具體的請求其實是:http://localhost:8080/message/nums/,

然后我們再全局搜索/message/nums找到發(fā)起這個異步請求的js地方:

所以我們確定了這個彈窗應(yīng)該就是這引起的了。所以我們?nèi)懸幌逻@個方法。這是新消息通知,我們之前在用戶中心弄過一個我的消息,但是好像沒有狀態(tài)(已讀和未讀),所以我需要在UserMessage上添加一個status字段標(biāo)識已讀和未讀。記得數(shù)據(jù)庫要添加字段。

  • com.example.entity.UserMessage

/*** 狀態(tài):0未讀,1已讀*/ private Integer status;

然后我們再查下當(dāng)前用戶的狀態(tài)未0的消息數(shù)量出來,就是新消息通知的數(shù)量了。

  • 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(); }

返回值是啥我是根據(jù)js推算出來的,js需要啥結(jié)果我就返回啥結(jié)果。重新運(yùn)行代碼之后,我們發(fā)現(xiàn)彈窗沒有了,頁面展示效果如下:?

消息通知

至此,新消息通知已經(jīng)ok,接下來我們搞一個高大上一點的功能。我們刷微博簡書頭條等網(wǎng)站的時候,如果收到消息通知,一般來說不用我們刷新頁面,而是實時給我們展示有消息來了,會突然有個新消息通知的圖標(biāo)提示我們,這是怎么做到的呢,結(jié)合我們之前學(xué)過的知識。我們可以找到幾種方案來實現(xiàn)這個功能:

  • ajax定時加載刷新

  • websocket雙工通訊

  • 長鏈接

我們課程中有節(jié)課是專門講解websocket的,接下來我們就使用這個技術(shù)來實現(xiàn)這個功能。

同學(xué)們可以去回顧一下websocket的知識:

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

上面是一個springboot集成ws的demo,接下來我們安裝這個例子的步驟把ws集成到我們現(xiàn)有的項目里面。

第一步:導(dǎo)入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協(xié)議來傳輸基于代理的消息,Broker就是代理的意思。 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {/**** 注冊 Stomp的端點* addEndpoint:添加STOMP協(xié)議的端點。提供WebSocket或SockJS客戶端訪問的地址* withSockJS:使用SockJS協(xié)議* @param registry*/public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/websocket").withSockJS();}/*** 配置消息代理* 啟動Broker,消息的發(fā)送的地址符合配置的前綴來的消息才發(fā)送到這個broker*/public void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/user/", "/topic");//推送消息前綴registry.setApplicationDestinationPrefixes("/app");} }

我們來解析一下這是啥意思,首先@EnableWebSocketMessageBroker,springboot的手動配置大家還記得吧?這個也是開啟ws消息代理,然后繼承WebSocketMessageBrokerConfigurer重寫registerStompEndpoints和configureMessageBroker方法,registerStompEndpoints方法是注冊端點,addEndpoint("/websocket")表示注冊一個端點叫websocket,那么前端就能通過這個鏈接連接到服務(wù)器實現(xiàn)雙工通訊了。.withSockJS()意思是使用SockJs協(xié)議,回顧一下:

  • SockJs是解決瀏覽器不支持ws的情況

  • Stompjs是簡化文本傳輸?shù)母袷?/p>

configureMessageBroker是配置消息代理,上面我們配置了/user,/topic都是需要消息代理的鏈接。前端/app鏈接前綴過來的消息都會進(jìn)入消息代理。

有了這兩步驟后,我們就可以使用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判斷,我是根據(jù)收藏那里來寫的,var socket = new SockJS("/websocket");表示建立端點鏈接,這樣前端就會和后端建立ws雙工通道,stompClient = Stomp.over(socket);表示切換成stomp文本傳輸協(xié)議傳輸內(nèi)容。stompClient.connect表示建立連接觸發(fā)的方法,這個方法里面有個stompClient.subscribe,差不多就表示訂閱這個消息隊列的意思,當(dāng)后端往/user/{userId}/messCount里面發(fā)送消息時候,當(dāng)前用戶就能接收到消息,res.body就是返回的內(nèi)容,然后就是showTips方法,這個方法其實就是渲染新消息通知的樣式,我們從之前的新消息通知那里吧對應(yīng)的js復(fù)制過來即可:

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,那前端我們已經(jīng)可以連上ws實現(xiàn)雙工通訊,并且監(jiān)聽了/user/{userId}/messCount這個隊列,所以后端往這里面發(fā)送消息前端就能收到然后實現(xiàn)showTips方法。 那后端什么時候該發(fā)送消息給前端呢?

  • 有人評論了作者文章,或者回復(fù)作者的評論

  • 系統(tǒng)消息等

ok,我們先來寫一個wsService,寫一個發(fā)送消息數(shù)量給前端的方法。

  • com.example.service.WsService

void sendMessCountToUser(Long userId, Integer count);

他的實現(xiàn)類復(fù)雜嘛?其實不復(fù)雜,我們先來看下參數(shù),userId,就是限定要給誰發(fā)送消息,count是消息數(shù)量,這里我們考慮多種情況,但count不為空時候,我們返回count數(shù)量的,當(dāng)count為空時候,我們搜索userId所有未讀的消息數(shù)量然后返回。

  • com.example.service.impl.WsServiceImpl

@Slf4j @Service public class WsServiceImpl implements WsService {@Autowiredprivate SimpMessagingTemplate messagingTemplate ;@AutowiredUserMessageService userMessageService;/*** 訂閱鏈接為/user/{userId}/messCount的用戶能收到消息* /user為默認(rèn)前綴** @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發(fā)送消息成功------------> {}, 數(shù)量:{}", userId, count);} }

發(fā)送ws消息,用的是SimpMessagingTemplate,convertAndSendToUser方法會自動在前面添加前綴/user,然后是userId,加上后面的后綴/messCount,所以加起來的鏈接其實就是/user/{userId}/messCount,那么我們在需要發(fā)送消息的地方調(diào)用這個方法即可。 然后這里還有個內(nèi)容要點,就是這里我用了一個@Async表示異步,從線程角度來說就是新起一個線程來執(zhí)行這個方法,從而保證不影響調(diào)用方的事務(wù)和執(zhí)行時間等。

那么我們來說下@Async的用法

異步@Async

其實這里我原本是想用隊列來實現(xiàn)的,也能表示異步。本著讓同學(xué)們接觸到更多知識,我們這里就用了@Aysnc注解來實現(xiàn),后面我們還是會用到MQ的,同學(xué)們別急。

使用這個注解我們需要開啟異步配置。注解是@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注解來實現(xiàn)異步了,asyncTaskExecutor()其實就是我用來重寫AsyncTaskExecutor用的,定義了最大線程組等信息。另外Async其實還可以配置很多信息,比如異步線程出錯時候的處理(重試等),大家課后可以多查詢一下資料,這注解我在工作中運(yùn)用其實還比較多的。 好了,上面我們已經(jīng)把發(fā)送ws消息的方法改成了異步方法,會起一個線程執(zhí)行發(fā)送。我們現(xiàn)在需要調(diào)用的地方其實就是在評論那里。

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

這樣有人評論文章或者回復(fù)評論的時候,都能實時收到消息了,我們來演示一下效果:

至此,我們實現(xiàn)了傳說中的實時通知功能!要膨脹了~

文章閱讀量

接下來的任務(wù),我們是要完善一下文章閱讀量。之前訪問文章,閱讀量都沒增加,現(xiàn)在我們來補(bǔ)上 這個一個bug。怎么做呢?是每訪問一次我們就直接修改數(shù)據(jù)庫?這里我們使用緩存在解決這個問題,每次訪問,我們就直接緩存的閱讀量增一,然后在某一時刻再同步到數(shù)據(jù)庫中即可。訪問文章時候,我們把緩存中的閱讀量傳到vo中,具體咋樣的呢,我們找到之前寫的com.example.controller.PostController#view方法,然后我加了這一句代碼:

技術(shù)要把vo的viewCount值修改成緩存的數(shù)量。

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

@Override public void setViewCount(Post post) {// 從緩存中獲取閱讀數(shù)量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);}// 設(shè)置新的閱讀redisUtil.hset("rank_post_" + post.getId(), "post:viewCount", post.getViewCount()); }

從代碼中可以看到,我們先從緩存中獲取ViewCount,然后設(shè)置post.setViewCount,最后再把加一之后的值同步到redis中。 ok,這一步還是比較簡單的,接下來我們要起一個定時器,然后定時吧緩存中的閱讀量同步到數(shù)據(jù)庫中,實現(xiàn)數(shù)據(jù)同步。

  • com.example.schedules.ScheduledTasks

@Slf4j @Component public class ScheduledTasks {@AutowiredRedisUtil redisUtil;@Autowiredprivate RedisTemplate redisTemplate;@AutowiredPostService postService;/*** 閱讀數(shù)量同步任務(wù)* 每天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) {// 刪除緩存中的閱讀數(shù)量,防止重復(fù)同步(根據(jù)實際情況來)redisUtil.hdel("rank_post_" + post.getId(), "post:viewCount");}}log.info("同步文章閱讀成功 ------> {}", syncKeys);} }

為何獲取所有需要同步閱讀的列表,我們用了keys命令,實際上當(dāng)redis的緩存越來越大的時候,我們是不能再使用這keys命令的,因為keys命令會檢索所有的key,是個耗時的過程,而redis又是個單線程的中間件,會影響其他命令的執(zhí)行。所以理論上我們需要用scan命令。考慮到這里博客只是個簡單業(yè)務(wù),redis不會很大,所以就直接用了keys命令,后期大家可以自行優(yōu)化。 然后獲取到列表后,然后就是獲取所有的實體,然后批量更新閱讀量。

?給eblog一個star,感謝支持哈

總結(jié)

以上是生活随笔為你收集整理的(eblog)8、消息异步通知、细节调整的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。