SpringBoot与webSocket实现在线聊天室——实现私聊+群聊+聊天记录保存

引用参考:原文章地址:https://blog.csdn.net/qq_41463655/article/details/92410518

在此基础上实现对聊天记录的保存。

代码地址:链接:https://pan.baidu.com/s/1IJFZDa4S_DF08773sKJWeA 提取码:jkui
思路:新建一个实体类用于保存聊天记录,在消息发送时,设置对象的各个值然后保存到数据库中。
实现步骤:
  1. 实体类: 新建实体类UserMessage,根据需要设置对象的字段,如:发送者、接收者、信息内容、发送时间等。
  2. mapper层:写UserMessageMapper接口,继承BaseMapper(我使用的是mybatis-plus,也可以使用其他方法)。
  3. service层:UserMessageService继承IService,service实现类UserMessageServiceImpl继承ServiceImpl<UserMessageMapper, UserMessage> 实现UserMessageService。
  4. controller层:在UserMessageController中的onMessage方法中,将前端传过来的json解析为字段,新建UserMessage对象message1,设置message1对象的各个值,通过消息接收者类型的判断,确实数据库中接收者的值如何设置,然后将UserMessage对象保存到数据库中。
此时,聊天记录保存到数据库时,是一条一条进行保存,为了减少对数据库频繁的操作,在UserMessageController类中定义一个静态的集合MessageList用来暂时保存聊天记录,并且设置MessageList集合的长度为一个固定值不可变,目的是为了实现将聊天记录保存到集合中,当集合的长度等于设定的值时,批量将聊天记录保存到数据库中。
  1. 所以将聊天记录保存到数据库前,先将聊天记录保存到集合MessageList中,判断集合的长度,如果集合的长度等于设定的值,将集合中的数据批量保存到数据库中,然后清空集合,为下一次保存聊天记录做准备。
我在实现保存聊天记录功能时遇到的问题:
  1. 代码没有错,群聊、私聊都可以实现,但是无法保存聊天记录,错:设置的表名与数据库没有对应好。
  2. 保存聊天记录的集合一定要设置为全局,如果是在方法中新建集合,那么聊天记录保存的并不是同一个集合,导致集合的长度永远无法达到设定的值,无法批量保存,导致数据保存失败。
  3. 当聊天页面关闭时,聊天记录的数据就会丢失,为什么不在UserMessageController中的onClose方法中判断在关闭聊天框前,集合中是否有聊天记录,如果有则进行保存??因为设置的集合为全局变量,就算是关闭了聊天页面,聊天记录也会暂时保存到集合中,并不会丢失。
效果展示图:

页面展示
实现聊天
数据库信息

完整代码实现如下:
包结构图:

websocket

代码部分:
webSocket配置类:WebSocketConfig.java
package com.example.springboot_websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author Administrator
 */

@Configuration
@Component
public class WebSocketConfig {

    /**
     * 服务器节点
     *
     * 如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
实体类:UserMessage.java 定义实体类与数据库连接,存储聊天记录
package com.example.springboot_websocket.bean;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author Administrator
 */

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserMessage {

    /**
     * 消息发送者
     */
    private String username;

    /**
     * 聊天文本
     */
    private String message;

    /**
     * 消息接受者
     */
    private String tousername;

    /**
     * 发送时间
     */
    private Date createtime;
}
mapper层:UserMessageMapper.java
package com.example.springboot_websocket.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springboot_websocket.bean.UserMessage;
import org.apache.ibatis.annotations.Mapper;

/**
 * extends BaseMapper<T>:继承接口即可实现基本的CRUD方法
 * <p>
 * 1.如果自定义的mapper继承了mybatis-plus的BaseMapper时,xxxMapper.xml中不可以包含insert方法,因为在BaseMapper中存在该方法
 * 2.如果对应的xxxMapper.xml中包含insert方法,那么就会执行xxxMapper.xml中的方法,相当于重写BaseMapper中的insert方法;
 * 如果xxxMapper.xml中没有insert方法,默认使用BaseMapper中的方法
 * 3.注意:BaseMapper<T>中的泛型对应相应的实体类
 */

/**
 * @author Administrator
 */

@Mapper
public interface UserMessageMapper extends BaseMapper<UserMessage> {

}
service层:UserMessageService.java
package com.example.springboot_websocket.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.springboot_websocket.bean.UserMessage;

/**
 * @author Administrator
 */
public interface UserMessageService extends IService<UserMessage> {

}
service层:UserMessageServiceImpl .java
package com.example.springboot_websocket.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.springboot_websocket.bean.UserMessage;
import com.example.springboot_websocket.mapper.UserMessageMapper;
import org.springframework.stereotype.Service;

/**
 * @author Administrator
 */

@Service
public class UserMessageServiceImpl extends ServiceImpl<UserMessageMapper, UserMessage> implements UserMessageService {

}
controller层:UserMessageController.java
package com.example.springboot_websocket.controller;

import com.alibaba.fastjson.JSON;
import com.example.springboot_websocket.bean.UserMessage;
import com.example.springboot_websocket.service.UserMessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * websocket类
 *
 * @ServerEndpoint: socket链接地址
 */

/**
 * @author Administrator
 */

@ServerEndpoint("/websocket/{username}")
@Controller
@Component
public class UserMessageController {

    /**
     * 设置一次性存储数据的list的长度为固定值,每当list的长度达到固定值时,向数据库存储一次
     */
    private static final Integer LIST_SIZE = 3;

    /**
     * 设置在线人数为静态变量
     */
    public static int onlineNumber = 0;

    private static UserMessageService userMessageService;

    /**
     * 新建list集合存储数据
     */
    private static ArrayList<UserMessage> MessageList = new ArrayList<>();

    /**
     * map(username,websocket)作为对象添加到集合中
     */
    private static Map<String, UserMessageController> clients = new ConcurrentHashMap<String, UserMessageController>();

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * session会话
     */
    private Session session;
    /**
     * 用户名称
     */
    private String username;

    public static synchronized int getOnlineCount() {
        return onlineNumber;
    }

    @Autowired
    public void setOgLocationService(UserMessageService userMessageService) {
        UserMessageController.userMessageService = userMessageService;
    }

    /**
     * 进入聊天室 --> 项目中读取用户信息获取用户名
     */
    @RequestMapping("/websocket")
    public String webSocket(Model model) {

        //根据时间随机定义名称
        String name = "游客:";
        String datename = new SimpleDateFormat("msss").format(new Date());
        name = name + datename;

        //websock链接地址+游客名-->  项目中请定义在配置文件 -->或直接读取服务器,ip 端口
        String path = "ws://127.0.0.1:8080/websocket/";
        model.addAttribute("path", path);
        model.addAttribute("username", name);
        return "socket";
    }

    /**
     * 监听连接(有用户连接,立马到来执行这个方法)
     * session 发生变化
     *
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) {

        //每打开一个新的窗口,在线人数onlineNumber++
        onlineNumber++;
        //把新用户名赋给变量
        this.username = username;
        //把新用户的 session 信息赋给变量
        this.session = session;
        //输出 websocket 信息
        logger.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
        logger.info("有新连接加入! 当前在线人数" + onlineNumber);
        try {
            //把自己的信息加入到map当中去,this=当前类(把当前类作为对象保存起来)
            clients.put(username, this);
            //获得所有的用户lists
            Set<String> lists = clients.keySet();

            // 发送全体信息(新用户上线信息)

            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            Map<String, Object> map1 = new HashMap(100);
            //  把所有用户列表
            map1.put("onlineUsers", lists);
            //  返回上线状态
            map1.put("messageType", 1);
            //  返回用户名
            map1.put("username", username);
            //  返回在线人数
            map1.put("number", onlineNumber);

            //  发送全体信息(用户上线信息)
            sendMessageAll(JSON.toJSONString(map1), username);

            // 给自己发一条消息:告诉自己现在都有谁在线
            Map<String, Object> map2 = new HashMap(100);
            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            map2.put("messageType", 3);
            //把所有用户放入map2
            map2.put("onlineUsers", lists);
            //返回在线人数
            map2.put("number", onlineNumber);

            // 消息发送指定人(所有的在线用户信息)
            sendMessageTo(JSON.toJSONString(map2), username);
        } catch (IOException e) {
            logger.info(username + "上线的时候通知所有人发生了错误");
        }
    }

    /**
     * 监听连接断开(有用户退出,会立马到来执行这个方法)
     */
    @OnClose
    public void onClose() {
        //每关闭一个新的窗口,在线人数onlineNumber--
        onlineNumber--;
        //从所有在线用户的map中去除下线用户
        clients.remove(username);
        try {
            //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息
            Map<String, Object> map1 = new HashMap(100);
            map1.put("messageType", 2);
            //所有在线用户
            map1.put("onlineUsers", clients.keySet());
            //下线用户的用户名
            map1.put("username", username);
            //返回在线人数
            map1.put("number", onlineNumber);
            //发送信息,所有人,通知谁下线了
            sendMessageAll(JSON.toJSONString(map1), username);
            //关闭连接前,判断list集合是否有数据,如果有,批量保存到数据库
            if (MessageList.size() < LIST_SIZE) {
                userMessageService.saveBatch(MessageList);
            }
        } catch (IOException e) {
            logger.info(username + "下线的时候通知所有人发生了错误");
        }
        logger.info("有连接关闭! 当前在线人数" + onlineNumber);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("服务端发生了错误" + error.getMessage());
        //error.printStackTrace();
    }

    /**
     * 监听消息(收到客户端的消息立即执行)
     *
     * @param message 消息
     * @param session 会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            logger.info("来自客户端消息:" + message + "客户端的id是:" + session.getId());
            //用户发送的信息
            com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(message);
            //发送的内容
            String textMessage = jsonObject.getString("message");
            //发送人
            String fromusername = jsonObject.getString("username");
            //接收人  to=all 发送消息给所有人 || to= !all   to == 用户名
            String tousername = jsonObject.getString("to");

            //新建message对象
            UserMessage message1 = new UserMessage();

            //设置发送者的username
            message1.setUsername(fromusername);
            //设置发送的信息
            message1.setMessage(textMessage);
            //设置发送时间
            message1.setCreatetime(new Date());
            //判断接收者
            if (tousername.equals("All")) {
                message1.setTousername("All");
            } else {
                message1.setTousername(tousername);
            }
            //批量保存信息
            //将每条记录添加到list集合中
            MessageList.add(message1);
            //判断list集合长度
            if (MessageList.size() == LIST_SIZE) {
                userMessageService.saveBatch(MessageList);
                //清空集合
                MessageList.clear();
            }


            //发送消息  -- messageType 1代表上线 2代表下线 3代表在线名单  4代表消息
            Map<String, Object> map1 = new HashMap(100);
            map1.put("messageType", 4);
            map1.put("textMessage", textMessage);
            map1.put("fromusername", fromusername);
            if (tousername.equals("All")) {
                //消息发送所有人(同步)
                map1.put("tousername", "所有人");
                sendMessageAll(JSON.toJSONString(map1), fromusername);
            } else {
                //消息发送指定人(同步)
                map1.put("tousername", tousername);
                sendMessageTo(JSON.toJSONString(map1), tousername);
            }
        } catch (Exception e) {
            logger.info("发生了错误了");
        }
    }

    /**
     * 消息发送指定人
     */
    public void sendMessageTo(String message, String toUserName) throws IOException {
        //遍历所有用户
        for (UserMessageController item : clients.values()) {
            if (item.username.equals(toUserName)) {
                //消息发送指定人(同步)
                item.session.getBasicRemote().sendText(message);
                break;
            }
        }
    }
    /**
     * 消息发送所有人
     */
    public void sendMessageAll(String message, String fromUserName) throws IOException {
        for (UserMessageController item : clients.values()) {
            //消息发送所有人(同步)getAsyncRemote
            item.session.getBasicRemote().sendText(message);
        }
    }
}
启动类:SpringbootWebsocketApplication.java
package com.example.springboot_websocket;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Administrator
 */
@MapperScan("com.example.springboot_websocket.Mapper")
@SpringBootApplication
public class SpringbootWebsocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebsocketApplication.class, args);
    }
}
配置文件:application.yml
spring:
  datasource:
    username: root
    password: 123456
    #创建数据库连接,连接到springboot数据库
    url: jdbc:mysql://localhost:3306/websocket?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
前端页面:socket.html
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta content="webkit" name="renderer">
    <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
    <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
    <link href="../frame/layui/css/layui.css" rel="stylesheet">
    <link href="../frame/static/css/style.css" rel="stylesheet">
    <link href="../frame/static/image/code.png" rel="icon">
    <title>Chatting Room</title>
    <script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js" type="text/javascript"></script>
    <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
</head>
<css>

</css>

<body>


<!--socket url-->
<input id="path" style="display: none" th:value="${path}" type="hidden"/>
<!--  用户名 -->
<input id="username" style="display: none" th:value="${username}" type="hidden"/>


<!-- ===============================================================================================================  -->
<br>

<!--<input  type="hidden" value="所有人" id="onLineUser" text="所有人" style="display: none" />-->

<!-- ===============================================================================================================  -->

<!--    overflow-y :auto;   overflow :auto;  宽高自适应滚动条-->
<h2 align="center">聊天室</h2>

<span id="miqx"
      style="width:80%; height:350px; background-color: LightBlue;  float:left; overflow-y :auto; overflow :auto;">
    <li style="text-align: center">群聊信息</li>
    <hr/>
</span>
<span id="miax"
      style="width:20%; height:350px; background-color: LightSteelBlue; float:left;overflow-y :auto; overflow :auto;">
    <li style="text-align: center">在线列表</li>
    <hr/>
</span>

<textarea cols="35" id="text" placeholder="请输入内容" rows="3"></textarea>
<input onclick="send()" type="button" value="发送">

<td>消息发送至:</td>
<select id="onLineUser" size="1" style="width:20%;height:20px">
    <option value="所有人">所有人</option>
</select>


<div id="mizx" style="width:80%;height:300px;background-color: LightBlue;float:left;overflow-y :auto;overflow :auto;">
    <li style="text-align: center">私聊信息</li>
    <hr/>
</div>
<br>
<br>
<br><br><br>

<!-- ===============================================================================  -->

</body>


<script type="text/javascript">

    function uaername(name) {
        alert(name)
    }

    var miqx = $("#miqx");  //群聊
    var miax = $("#miax");  //在线列表
    var mizx = $("#mizx");  //私聊
    var onLineUser = $("#onLineUser");    //发送人select选择框

    var webSocket;

    var commWebSocket;
    http:
        if ("WebSocket" in window) {
            //127.0.0.1:8080/
            webSocket = new WebSocket(document.getElementById('path').value + document.getElementById('username').value);
            //连通之后的回调事件
            webSocket.onopen = function () {
                //连接成功向页面+消息提示
                miqx.html(miqx.html() + " <li style='text-align: center'>系统消息:[登陆成功]</li>")
            };

            //接收后台服务端的消息
            webSocket.onmessage = function (evt) {
                var received_msg = evt.data;           //接收到的数据
                var obj = JSON.parse(received_msg);    //json数据
                var messageType = obj.messageType;      //数据类型(1上线/2下线/3在线名单/4发信息)
                var onlineName = obj.username;         //用户名
                var number = obj.number;               //在线人数

                //上线通知+在线列表刷新
                if (obj.messageType == 1) {
                    if ((onlineName != $("#username").val())) { //展示除不等于自己的所有用户
                        miqx.html(miqx.html() + " <li style='text-align: center'>系统消息:[" + onlineName + "]上线" + "</li>");
                        onLineUser.html(onLineUser.html() + "<option  value='" + onlineName + "'>" + onlineName + "</option>");
                    }
                    var onlineName = obj.onlineUsers;  //所有在线用户
                    miax.html("<li style='text-align: center'>在线用户&nbsp;&nbsp;[" + onlineName.length + "]&nbsp;&nbsp;</li>");
                    for (var i = 0; i < onlineName.length; i++) {
                        if ((onlineName[i] != $("#username").val())) { //展示除不等于自己的所有用户
                            miax.html(miax.html() + "<li style='text-align: left'>&nbsp;&nbsp;" + onlineName[i] + "</li>");
                        }
                    }
                    //miax.html(miax.html()+" <li style='text-align: center'>"+ onlineName +"</li>");
                }

                //下线通知+在线列表刷新
                else if (obj.messageType == 2) {
                    if ((onlineName != $("#username").val())) { //展示除不等于自己的所有用户
                        miqx.html(miqx.html() + " <li style='text-align: center'>系统消息:[" + onlineName + "]下线" + "</li>");
                    }
                    var onlineName = obj.onlineUsers;  //剩余所有在线用户
                    miax.html("<li style='text-align: center'>在线用户&nbsp;&nbsp;[" + onlineName.length + "]&nbsp;&nbsp;</li>");
                    onLineUser.html("<option  value='所有人'>所有人</option>");
                    for (var i = 0; i < onlineName.length; i++) {
                        if ((onlineName[i] != $("#username").val())) { //展示除不等于自己的所有用户
                            miax.html(miax.html() + "<li style='text-align: left'>&nbsp;&nbsp;" + onlineName[i] + "</li>");
                            onLineUser.html(onLineUser.html() + "<option  value='" + onlineName[i] + "'>" + onlineName[i] + "</option>");
                        }
                    }
                }

                //在线列表
                else if (obj.messageType == 3) {
                    var onlineName = obj.onlineUsers; //所有在线用户
                    miax.html("<li style='text-align: center'>在线用户&nbsp;&nbsp;[" + onlineName.length + "]&nbsp;&nbsp;</li>");

                    onLineUser.html("<option  value='所有人'>所有人</option>");
                    for (var i = 0; i < onlineName.length; i++) {
                        if (onlineName[i] != $("#username").val()) { //展示除不等于自己的所有用户
                            miax.html(miax.html() + " <li style='text-align: left'>&nbsp;&nbsp;" + onlineName[i] + "</li>");
                            onLineUser.html(onLineUser.html() + "<option  value='" + onlineName[i] + "'>" + onlineName[i] + "</option>");
                        }
                    }
                }
                //信息接收
                else {
                    var time2 = new Date();
                    var date = time2.getHours() + ":" + time2.getMinutes() + ":" + time2.getSeconds();  //时间
                    if (obj.fromusername != $("#username").val()) {    //自己不接自己的消息
                        if (obj.tousername == "所有人") {
                            //发给所有人
                            miqx.html(miqx.html() + " <li style='text-align: left'>[" + obj.fromusername + "]说:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + obj.textMessage + "</li>");
                        } else {
                            //发给指定人
                            mizx.html(mizx.html() + " <li style='text-align: left'>[" + obj.fromusername + "]说:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + obj.textMessage + "</li>");
                        }
                    }
                    //setMessageInnerHTML(obj.fromusername+"对"+obj.tousername+"说:"+obj.textMessage);
                }
            };

            //连接关闭的回调事件
            webSocket.onclose = function () {
                console.log("连接已关闭...");
                setMessageInnerHTML("连接已经关闭....");
            };
        } else {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    function closeWebSocket() {
        //直接关闭websocket的连接
        webSocket.close();
    }

    //信息发送+ 页面显示发送信息
    $(document).keyup(function (event) {
        //浏览器适应
        if (event.ctrlKey && event.which == 13 || event.which == 10) {
            send();
        } else if (event.shiftKey && event.which == 13 || event.which == 10) {
            send();
        }
    });

    //信息发送+ 页面显示发送信息
    function send() {
        var usernameX = $("#username").val()         //发送数据人
        var usernameY = $("#onLineUser").val();      //接收数据人
        var message = $("#text").val();               //发送的数据
        if (usernameY == "所有人") {
            usernameY = "All";
            /*  <li style="text-align: center">群聊信息</li>
             <li style="text-align: right">靠右</li>
             <li style="text-align: left" >靠左</li>*/
            // miqx.html(miqx.html()+" <li style='text-align: right'>"+ message+" <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ["+usernameX +"]</li>");
            miqx.html(miqx.html() + " <li style='text-align: right'>" + "[" + usernameX + "]说:&nbsp;&nbsp;&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + message + "</li>");
        } else {
            mizx.html(mizx.html() + " <li style='text-align: right'>" + "你对-[" + usernameY + "]说:&nbsp;&nbsp;&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + message + "</li>");
        }
        var message = {
            "message": message,
            "username": usernameX,
            "to": usernameY
        };
        //发送数据
        webSocket.send(JSON.stringify(message));
        $("#text").val("");
    }

    layui.use(['form', 'layedit', 'laydate'], function () {
        var form = layui.form
            , layer = layui.layer
            , layedit = layui.layedit
            , laydate = layui.laydate;

        //监听指定开关
        form.on('switch(switchTest)', function (data) {
            layer.msg('你以' + (this.checked ? '上线' : '下线'), {
                offset: '6px'
            });
            //layer.tips('温馨提示:请注意开关状态的文字可以随意定义,而不仅仅是ON|OFF', data.othis)
        });
    });
</script>
</html>
Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐