# Part01-集成WeSocket实现用户评论-即时通讯

blog
│  pom.xml
│
└─src
│  └─main
│      ├─java
│      │   └─org
│      │      └─myslayers
│      │          ├─config
│      │          │      WsConfig.java
│      │          │
│      │          ├─controller
│      │          │      BaseController.java
│      │          │      PostController.java
│      │          │
│      │          ├─service
│      │          │  │   WsService.java
│      │          │  │
│      │          │  └─impl
│      │          │         WsServiceImpl.java
│      │
│      └─resources
│          ├─templates
│          │  └─inc
│          │         layout.ftl

# 1.1 集成 WebSocket 环境

  • pom.xml :项目依赖,【websocket 通讯】
<dependencies>
  <!--websocket-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>
</dependencies>

# 1.2 配置 WebSocket 环境

  • WsConfig.java :配置类,【点对点通讯,订阅通道/user/、/topic/,访问地址/websocket】
/**
 * WebSocket 配置类:点对点
 */
@EnableAsync //开启异步消息
@Configuration
@EnableWebSocketMessageBroker //表示开启使用STOMP协议的消息代理
public class WsConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket") // 注册一个端点:websocket的访问地址
            .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/user/", "/topic/"); //推送消息的前缀
        registry.setApplicationDestinationPrefixes("/app"); //注册代理点
    }
}
  • layout.ftl :模板引擎,【引入 sockjs.js、stomp.js】
<#macro layout title>
  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>${title}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="keywords" content="fly,layui,前端社区">
    <meta name="description" content="Fly社区是模块化前端UI框架Layui的官网社区,致力于为web开发提供强劲动力">
    <link rel="stylesheet" href="/res/layui/css/layui.css">
    <link rel="stylesheet" href="/res/css/global.css">

    <#--导入script-->
    <script src="/res/layui/layui.js"></script>
    <script src="/res/js/jquery.min.js"></script>
    <script src="/res/js/sockjs.js"></script>
    <script src="/res/js/stomp.js"></script>
  </head>
  <body>
</#macro>

# 1.3 使用 WebSocket 通讯

  • PostController.java :控制层,【即时通知作者(websocket)】
@Controller
public class PostController extends BaseController {
    /**
     * 详情detail:【评论】文章
     */
    @ResponseBody
    @Transactional
    @PostMapping("/post/reply/")
    public Result reply(Long jid, String content) {
        Assert.notNull(jid, "找不到对应的文章");
        Assert.hasLength(content, "评论内容不能为空");

        Post post = postService.getById(jid);
        Assert.isTrue(post != null, "该文章已被删除");

        // 新增评论
        Comment comment = new Comment();
        comment.setPostId(jid);
        comment.setContent(content);
        comment.setUserId(getProfileId());
        comment.setCreated(new Date());
        comment.setModified(new Date());
        comment.setLevel(0);
        comment.setVoteDown(0);
        comment.setVoteUp(0);
        commentService.save(comment);

        // 评论数量+1
        post.setCommentCount(post.getCommentCount() + 1);
        postService.updateById(post);

        // 本周热议数量+1
        postService.incrCommentCountAndUnionForWeekRank(post, true);

        // 通知作者,有人评论了你的文章
        // 作者自己评论自己文章,不需要通知
        if (comment.getUserId() != post.getUserId()) {
            UserMessage message = new UserMessage();
            message.setFromUserId(getProfileId());
            message.setToUserId(post.getUserId());
            message.setPostId(jid);
            message.setCommentId(comment.getId());
            message.setType(1); //我的消息的【消息的类型】:0代表系统消息、1代表评论的文章、2代表回复的评论
            message.setStatus(0); //我的消息的【状态】:0代表未读、1代表已读
            message.setCreated(new Date());
            messageService.save(message);

            // 即时通知【文章作者】(websocket)
            wsService.sendMessCountToUser(message.getToUserId());
        }

        // 通知被@的人,有人回复了你的文章
        if (content.startsWith("@")) {
            String username = content.substring(1, content.indexOf(" "));
            User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
            if (user != null) {
                UserMessage message = new UserMessage();
                message.setFromUserId(getProfileId());
                message.setToUserId(post.getUserId());
                message.setPostId(jid);
                message.setCommentId(comment.getId());
                message.setType(2); //我的消息的【消息的类型】:0代表系统消息、1代表评论的文章、2代表回复的评论
                message.setStatus(0); //我的消息的【状态】:0代表未读、1代表已读
                message.setCreated(new Date());
                messageService.save(message);

                // 即时通知【被@的用户】(websocket)
            }
        }

        return Result.success().action("/post/" + post.getId());
    }
}
  • WsServiceImpl.java :业务层实现,【使用 Spring 自带的【消息模板】,向 ToUserId 发生消息,url 为 /user/20/messCount/ 】
@Service
public class WsServiceImpl implements WsService {

    @Autowired
    UserMessageService messageService;

    @Autowired
    SimpMessagingTemplate messagingTemplate;    //Spring自带的【消息模板】

    @Async  //异步消息
    @Override
    public void sendMessCountToUser(Long toUserId) {
        int count = messageService.count(new QueryWrapper<UserMessage>()
            .eq("to_user_id", toUserId) //全部数量的消息
            .eq("status", "0")      //未读的消息  未读0 已读1
        );

        // websocket 使用 messagingTemplate模板 进行通知,拼凑结果url为:/user/20/messCount/
        // super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
        messagingTemplate.convertAndSendToUser(toUserId.toString(), "/messCount", count);
    }
}
  • layout.ftl :模板引擎
<#--宏:1.macro定义脚本,名为layout,参数为title-->
<#macro layout title>
  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>${title}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="keywords" content="fly,layui,前端社区">
    <meta name="description" content="Fly社区是模块化前端UI框架Layui的官网社区,致力于为web开发提供强劲动力">
    <link rel="stylesheet" href="/res/layui/css/layui.css">
    <link rel="stylesheet" href="/res/css/global.css">

    <#--导入script-->
    <script src="/res/layui/layui.js"></script>
    <script src="/res/js/jquery.min.js"></script>
    <script src="/res/js/sockjs.js"></script>
    <script src="/res/js/stomp.js"></script>
  </head>
  <body>

  <#--宏common.ftl:分页、一条数据posting、个人账户-左侧链接(我的主页、用户中心、基本设置、我的消息)-->
  <#include "/inc/common.ftl" /><#--经过测试,发现common公共包,必须在header.ftl等之前进行“include导入”-->

  <#--【一、导航栏】-->
  <#include "/inc/header.ftl"/>

  <#--【三、所有引用该“带有宏的标签layout.ftl”都会执行该操作:<@layout "首页"></@layout>中的数据 -> 填充到<#nested/>标签中】-->
  <#nested>

  <#--【四、页脚】-->
  <#include "/inc/footer.ftl"/>

  <script>
    <#-----------------方式二:利用session来实现【登录状态】---------------------->
    // layui.cache.page = '';
    layui.cache.user = {
      username: '${profile.username!"游客"}'
      , uid: ${profile.id!"-1"}
      , avatar: '${profile.avatar!"/res/images/avatar/00.jpg"}'
      , experience: 83
      , sex: '${profile.gender!"男"}'
    };

    layui.config({
      version: "3.0.0"
      , base: '/res/mods/' //这里实际使用时,建议改成绝对路径
    }).extend({
      fly: 'index'
    }).use('fly');
  </script>

  <script>
    <#--使用ws实现【评论消息的即时通讯】-->
    $(function () {
      var elemUser = $('.fly-nav-user');
      if (layui.cache.user.uid !== -1 && elemUser[0]) { //根据layui使用,layui.cache.user.uid !== -1 时,表示【用户登录成功】
        var socket = new SockJS("/websocket") //注册一个端点:websocket的访问地址
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
          //subscribe订阅消息
          stompClient.subscribe("/user/" + ${profile.id} + "/messCount", function (res) {
            console.log(res);
            showTips(res.body);   //消息的显示:弹窗
          })
        });

      }
    });

    //消息的显示:弹窗,【将/res/mods/index.js中的消息弹窗 -> 复制到此处,供ws使用】
    function showTips(count) {
      var msg = $('<a class="fly-nav-msg" href="javascript:;">' + count + '</a>');
      var elemUser = $('.fly-nav-user');
      elemUser.append(msg);
      //click,点击跳转【用户中心 /user/mess】
      msg.on('click', function () {
        location.href = "/user/mess";
      });
      //tips,提示【你有X条未读消息】
      layer.tips('你有 ' + count + ' 条未读消息', msg, {
        tips: 3
        , tipsMore: true
        , fixed: true
      });
      msg.on('mouseenter', function () {
        layer.closeAll('tips');
      })
    }
  </script>

  </body>
  </html>
</#macro>

# 1.4 其他:用户中心-批量将未读改为已读

  • UserController.java :控制层,【批量处理,将全部消息的【状态:未读 0】改为【状态:已读 1】,并【批量修改 状态为已读 1】】
@Controller
public class UserController extends BaseController {
    /**
     * 我的消息:查询消息
     */
    @GetMapping("/user/mess")
    public String mess() {
        IPage<UserMessageVo> page = messageService.paging(getPage(), new QueryWrapper<UserMessage>()
            .eq("to_user_id", getProfileId())
            .orderByDesc("created")
        );
        req.setAttribute("pageData", page);

        //查看消息时,将全部消息的【状态:未读0】改为【状态:已读1】,并【批量修改 状态为已读1】
        List<Long> ids = new ArrayList<>();
        for (UserMessageVo messageVo : page.getRecords()) {
            if (messageVo.getStatus() == 0) {
                ids.add(messageVo.getId());
            }
        }
        messageService.updateToReaded(ids); //批量处理

        return "/user/mess";
    }
}
  • UserMessageServiceImpl.java :业务层实现,【批量处理】
@Service
public class UserMessageServiceImpl extends ServiceImpl<UserMessageMapper, UserMessage> implements UserMessageService {

    @Override
    public void updateToReaded(List<Long> ids) {
        if (ids.isEmpty()) {
            return;
        }
        messageMapper.updateToReaded(new QueryWrapper<UserMessage>()
            .in("id", ids)
        );
    }
}
  • UserMessageMapper.java :数据层接口,【开启事务】
public interface UserMessageMapper extends BaseMapper<UserMessage> {

    @Transactional
    void updateToReaded(@Param(Constants.WRAPPER) QueryWrapper<UserMessage> wrapper);
}
  • UserMessageMapper.xml :数据层实现,【SQL 命令】
<update id="updateToReaded">
  UPDATE m_user_message
  SET status = 1
  ${ew.customSqlSegment}
</update>
Last Updated: 6/6/2024, 7:53:07 AM