网页版五子棋对战实现和自动化测试

news2024/11/25 16:35:06

文章目录

  • 前言
  • 一、项目描述
    • 项目演示链接
  • 二、实现的功能与操作
    • 1.登录注册
    • 2.游戏大厅
      • 线程安全问题
      • 多开处理
    • 3.五子棋对战
  • 三、项目测试
    • 1.测试用例
    • 2.测试技术点
    • 3.部分测试用例展示
      • (1)注册页面
      • (2)登录页面
      • (3)游戏大厅页面
      • (4)游戏对战页面
    • 4.项目测试的结果
      • 视频演示链接
  • 总结


前言

五子棋对战网页应用是一个基于Web技术的在线多人五子棋游戏。该应用提供了用户注册、登录、匹配对手、对战等功能,旨在为用户提供轻松愉快的游戏体验。本篇文章主要用来记录我的网页版五子棋项目,包括项目介绍、实现功能、测试用例、自动化测试等。


一、项目描述

该项目支持玩家随时随地与其他在线玩家进行五子棋对战,实现实时的棋局同步和即时通讯。利用 WebSocket 技术实现双向通信,保证游戏过程中的实时性和即时交互,提供更流畅的游戏体验。 提供游戏大厅,展示在线玩家信息、排名和当前对局状态。内置匹配系统,自动匹配合适的对手,确保玩家能够享受到公平有趣的游戏。

技术栈:
前端: 使用HTML5、CSS3、JavaScript以及现代前端框架(如React、Vue等)构建直观、响应式的用户界面。
后端: 基于Spring MVC、Spring Boot等后端框架,使用WebSocket实现实时通信,处理用户认证、游戏逻辑和数据存储。
数据库: 使用关系型数据库MySQL存储用户信息、对战历史等数据。
实时通信: 使用WebSocket技术,实现玩家之间的实时通信和对战同步。
webSocket:是一个应用层协议。四个核心点:建立连接之后、收到消息之后、出现连接异常、出现连接关闭。客户端通过webSocket.send()发送消息,服务器使用sendMessage()发送消息。

项目演示链接

二、实现的功能与操作

现在已实现注册登录功能、游戏匹配功能、下棋功能。

1.登录注册

登录主要是判定用户是不是合法登录;注册这一块比登录多一个确认密码,以及注册的这个人是不是在数据库中存在。
在这里插入图片描述后端代码如下(示例):


    @RequestMapping("/login")
    public Object login(HttpServletRequest request, String username, String password) {
        // 1.从数据库中找到匹配用户
        User user = userMapper.selectByName(username);
        System.out.println("当前登录用户:user=" + user);
        if (user == null || !user.getPassword().equals(password)) {
            // 登录失败
            System.out.println("登录失败!");
            return new User();
        }
        HttpSession httpSession = request.getSession(true);
        httpSession.setAttribute("user", user);
        return user;
    }

    @RequestMapping("/register")
    public Object register(String username, String password) {
        try {
            User user = new User();
            user.setUsername(username);
            user.setPassword(password);
            userMapper.insert(user);
            return user;
        } catch (org.springframework.dao.DuplicateKeyException e) {
            User user = new User();
            return user;
        }

    }

注册前端代码(示例):

    <script>
        function mysub() {
            var username = jQuery("#username");
            var password = jQuery("#password");
            var password2 = jQuery("#password2");
            if(username.val()=="") {
                alert("请先输入用户名!");
                username.focus();
                return;
            }
            if(password.val()=="") {
                alert("请先输入密码!");
                password.focus();
                return;
            }
            if(password2.val()=="") {
                alert("请确认密码!");
                password2.focus();
                return;
            }
            if(password2.val()!=password.val()) {
                alert("两次密码输入不一致!");
                password.focus();
                return;
            }
            jQuery.ajax({
                url:"/user/register",
                type:"POST",
                data:{"username":username.val(),"password":password.val()},
                success:function(result) {
                    if(result && result.username) {
                        if(confirm("恭喜:注册成功,是否跳转到登录页面?")) {
                            location.href = "/login.html";
                        }
                    }else {
                        alert("抱歉:注册失败,请重试!");
                    }
                }
            });
        }
    </script>

2.游戏大厅

游戏大厅需要先实现其大厅页面,里面包括玩家信息、开始匹配按钮,而最重要的则是一旦玩家点击开始匹配之后,需要给该按钮添加一个点击事件,这里就涉及我们的核心:游戏匹配,我们创建了两个个优先级队列,玩家登录成功将该玩家加入进一个玩家在线的HashMap,玩家点击匹配按钮后,将其加入另一个表示游戏匹配的HashMap。这里的我们需要考虑线程安全问题和多开问题。实现思路如下图所示:
在这里插入图片描述

游戏匹配后端代码如下(示例):

    // 建立连接之后
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 玩家上线,加入到onlineUserManager
        try {
            //1.先获取当前用户的身份信息
            // webSocketSession通过addInterceptors获取httpsession中的Attributes
            User user = (User) session.getAttributes().get("user");
            //2.判定当前用户是否是已经在线状态
            if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                    || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
                //说明当前用户已经登录!
                //告知客户端重复登录
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setReason("当前禁止多开!");
                response.setMessage("repeatConnection");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                // 直接关闭有些太激进
                //session.close();
                return;
            }
            //3.将玩家设置为在线状态
            onlineUserManager.enterGameHall(user.getUserId(), session);
            System.out.println("玩家:" + user.getUsername() + " 进入游戏大厅!");
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionEstablished]当前用户未登录");
        }
    }
    // 处理传输信息
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理开始匹配请求和处理停止匹配请求
        User user = (User) session.getAttributes().get("user");
        // 获取客户端发送给服务器的数据
        String payload = message.getPayload();
        // 将JSON搁置的字符串,转换为对象
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
            //(1)进入匹配队列
            //(2)创建一个类表示匹配队列,把当前用户加进去
            matcher.add(user);
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            // (1)退出匹配队列
            // (2)创建一个类表示匹配队列,把当前用户从队列中移除
            matcher.remove(user);
            // 移除之后,返回一个响应给客户端
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            // 非法情况
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }
    // 处理传输异常
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 玩家下线,从 onlineUserManager 中删除
        try {
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            // 保证退出的人和登录的人是一个人
            if (tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("玩家:" + user.getUsername() + " 退出游戏大厅!");
            }
            // 如果玩家正在匹配中,而 websocket 连接断开
            matcher.remove(user);
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.handleTransportError]当前用户未登录");
        }
    }
	// 处理传输关闭
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        try {
            // 玩家下线,从 onlineUserManager 中删除
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            // 保证退出的人和登录的人是一个人
            if (tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
                System.out.println("玩家:" + user.getUsername() + " 退出游戏大厅!");
            }
            // 如果玩家正在匹配中,而 websocket 连接断开
            matcher.remove(user);
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionClosed]当前用户未登录");
        }
    }

线程安全问题

我们对玩家进入游戏使用的是一个HashMap,玩家点击匹配按钮后,被加入另一个HashMap,而我们的系统涉及多个用户同时使用,则多线程访问同一个HashMap容易产生线程安全问题,这里我们将HashMap改为ConcurrentHashMap来处理线程安全问题,ConcurrentHashMap是线程安全的,它通过使用锁分段(segmented-locking)技术来提高并发访问性能。它允许多个修改操作并发进行,而不会导致数据不一致。
在这里插入图片描述

多开处理

(1)一个账户多个地方登录,那这种对于我们系统是不允许存在的,我们通过seesion判断当前用户是否存在,并且不允许一个客户端同时匹配两个用户。
在这里插入图片描述
(2)我的用户是按照分数划分为三个梯队,由于 handlerMatch 是在单独的线程中调用,因此要考虑到访问队列的线程安全问题.,需要加上锁。
在这里插入图片描述
在这里插入图片描述

3.五子棋对战

当游戏匹配已经有两个或两个以上的玩家,则会匹配成功进入同一个游戏房间,进行游戏对战。进入到游戏房间,这里的棋盘我们是用canvas画上去的,双方下棋的时候,使用websocket给双方用户通知提示当前对手信息,重写websocket四个核心方法,也要判断游戏输赢后通知给玩家。实现过程如下图所示:
在这里插入图片描述
重写websocket的四个核心方法,并实现通知玩家谁输谁赢的方法noticeThatUserWin。
游戏对战后端代码如下(示例):

Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        GameReadyResponse response = new GameReadyResponse();
        // 1.先获取到用户的身份信息,(从httpsession中拿到当前用户的对象)
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            response.setOk(false);
            response.setReason("用户尚未登录!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 2.判断当前用户是否已经进入房间
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 该玩家还没有匹配到对手,不应该进入房间
            response.setOk(false);
            response.setReason("用户尚未匹配到!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 3.判断当前是不是多开用户
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            response.setOk(true);
            response.setReason("禁止多开游戏页面");
            response.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 4.设置当前玩家上线
        onlineUserManager.enterGameRoom(user.getUserId(), session);
        // 5.把两个玩家加入到游戏房间中
        synchronized (room) {
            if (room.getUser1() == null) {
                room.setUser1(user);
                // 把先连入房间的玩家作为先手方
                room.setWhiteUser(user.getUserId());
                System.out.println("玩家1:" + user.getUsername() + " 进入游戏房间,已经准备就绪!");
                return;
            }
            if (room.getUser2() == null) {
                // 进入到这里说明user1已经进入房间
                room.setUser2(user);
                System.out.println("玩家2:" + user.getUsername() + " 进入游戏房间,已经准备就绪!");
                // 两个玩家就绪后
                // 通知玩家1
                noticeGameReady(room, room.getUser1(), room.getUser2());
                // 通知玩家2
                noticeGameReady(room, room.getUser2(), room.getUser1());
                return;
            }
        }
        // 6.此处如果又有玩家尝试连接同一个房间(理论上是不存在的)
        response.setOk(false);
        response.setReason("当前房间已满,您不能加入房间!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }

    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setOk(true);
        resp.setReason("");
        resp.setMessage("gameReady");
        resp.setReason(room.getRoomId());
        resp.setThisUserId(thisUser.getUserId());
        resp.setThatUserId(thatUser.getUserId());
        resp.setWhiteUser(room.getWhiteUser());
        // 把当前的响应数据传回给对应的玩家
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1.从session里拿到当前用户信息
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            System.out.println("[handleTextMessage] 当前玩家尚未登录!");
            return;
        }
        // 2.根据玩家id获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3.通过room对象来处理这次具体的请求
        room.putChess(message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户:" + user.getUsername() + " 游戏房间连接异常!");
        // 通知对手获胜了
        noticeThatUserWin(user);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户:" + user.getUsername() + " 离开游戏房间!");
        // 通知对手获胜了
        noticeThatUserWin(user);
    }
    // 判断输赢,并通知给玩家
    private void noticeThatUserWin(User user) throws IOException {
        // 1.根据当前玩家,找到玩家所在的房间
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            System.out.println("当前房间已经释放,无需通知对手!");
            return;
        }
        // 2.根据房间找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();
        // 3.找到对手的在线状态
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
        if (webSocketSession == null) {
            // 对手也掉线了
            System.out.println("对手已经掉线,无需通知!");
            return;
        }
        // 4.构造一个响应,通知对手,你是获胜方
        GameResponse resp = new GameResponse();
        resp.setMessage("putChess");
        resp.setUserId(thatUser.getUserId());
        resp.setWinner(thatUser.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
        // 5.更新玩家分数信息
        int winUserId = thatUser.getUserId();
        int loseUserId = user.getUserId();
        userMapper.userWin(winUserId);
        userMapper.userLose(loseUserId);

        // 6.释放房间对象
        roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());
    }

三、项目测试

1.测试用例

在这里插入图片描述

2.测试技术点

测试工具:Selenium4+Junit5
(1)使用定位元素定位、元素操作(findElement、cssSelector、sendKeys)实现页面交互;
(2)获取页面相关元素并使用断言(Assertions)判断元素或者文本信息与预期是否符合;
(3)使用@BeforeEach、@BeforeAll、@AfterAll等注解降低代码冗余;
(4)使用参数化方法,@CsvSource批量测试多组数据;
(5)使用@Order注解,设置测试内部执行顺序;
(7)使用套件(suit)将测试用例连起来,以便批量运行测试用例。

3.部分测试用例展示

(1)注册页面

根据对注册页面的测试用例编写,分别对注册页面可否正常打开、注册失败、注册成功进行测试。
这里写出测试注册成功的逻辑:
1)首先,清空登录框,以防多组登录;
2)使用参数测试,将CsvSource准备好的多组测试数据,使用cssSelector选择器选中输入框,用sendKey输入准备好的数据;
3)输入完注册信息后,再选中登录按钮后用click点击注册按钮;
4)由于点击注册按钮后会alert出一个弹窗提示,所以此时将driver切换到alert;
5)切换到alert后,提取到弹窗信息,根据弹窗信息判断是否注册成功;
6)注册成功后跳转到登录页面,此时使用写好的截图方法,记录下此刻。

    @ParameterizedTest
    @CsvSource({"刘云,123,1234"})
    @Order(2)
    public void registerFail(String name, String password, String password2) throws IOException {
        //清空登录框,以防多组登录
        driver.findElement(By.cssSelector("#username")).clear();
        driver.findElement(By.cssSelector("#password")).clear();
        driver.findElement(By.cssSelector("#password2")).clear();
        //进行注册
        driver.findElement(By.cssSelector("#username")).sendKeys(name);
        driver.findElement(By.cssSelector("#password")).sendKeys(password);
        driver.findElement(By.cssSelector("#password2")).sendKeys(password2);
        driver.findElement(By.cssSelector("#submit")).click();
        //driver转到alert
        Alert alert = driver.switchTo().alert();
        //获取弹窗中的提示信息
        String actual = alert.getText();
        if (actual.equals("请先输入用户名!")) {
            System.out.println("用户名为空!");
        } else if (actual.equals("请先输入密码!")){
            System.out.println("密码为空!");
        } else if (actual.equals("请确认密码!")) {
            System.out.println("再次输入密码为空!");
        } else if (actual.equals("两次密码输入不一致!")) {
            System.out.println("两次密码不一致!");
        } else if(actual.equals("抱歉:注册失败,请重试!")){
            System.out.println("已有该用户,请重试!");
        } else if(actual.equals("恭喜:注册成功,是否跳转到登录页面?")) {
            System.out.println("注册成功,跳转到登录页面!");
        } else {
            System.out.println("注册异常!");
        }
        alert.accept();
        getScreenShot(getClass().getName());
    }

(2)登录页面

根据测试用例,我们对登录页面是否正常、登录失败、登录成功分别测试。
这里写出测试登录失败的逻辑:
1)清空登录框。防止多组登录;
2)登录,使用cssSelector选择器和sendkeys输入文本,csvSource准备好测试数据;
3)获取弹窗和弹窗中的提示信息;
4)使用assertion判断实际信息和提示信息是否一致,一致则登录成功。

    @ParameterizedTest
    @CsvSource({"张三,1234"})
    @Order(2)
    public void loginFail(String name, String password) throws IOException {
        //登录失败
        try {
            // 清空登录框,以防多组登录
            driver.findElement(By.cssSelector("#username")).clear();
            driver.findElement(By.cssSelector("#password")).clear();

            //登录
            driver.findElement(By.cssSelector("#username")).sendKeys(name);
            driver.findElement(By.cssSelector("#password")).sendKeys(password);
            getScreenShot(getClass().getName());
            driver.findElement(By.cssSelector("#submit")).click();

            //获取警告框文本
            String expect = "登录失败!";
            Alert alert = driver.switchTo().alert();
            String actual = alert.getText();
            System.out.println(actual);

            Assertions.assertEquals(expect, actual);
            alert.accept();
        } catch (TimeoutException e) {
            System.out.println("登录失败:未出现警告框");
        } catch (UnhandledAlertException e) {
            System.out.println("警告框文本与预期不符:" + e.getMessage());
        }
    }

(3)游戏大厅页面

根据游戏大厅的测试用例,编写页面加载正常、检测用户登录、匹配按钮的测试代码。
其匹配按钮的测试代码逻辑如下:
1)由于点击匹配按钮需要是登录的状态下,所以先进行用户的登录;
2)登录后点击匹配按钮;
3)查看点击后的匹配按钮的状态是否发生变化;
4)再次点击按钮;
5)查看按钮是否恢复为开始匹配状态。

    @Test
    @Order(3)
    public void isMatch() throws IOException, InterruptedException {
        // 1.先登录
        driver.get("http://127.0.0.1:8081/login.html");
        driver.findElement(By.cssSelector("#username")).clear();
        driver.findElement(By.cssSelector("#password")).clear();
        driver.findElement(By.cssSelector("#username")).sendKeys("张三");
        driver.findElement(By.cssSelector("#password")).sendKeys("123");
        driver.findElement(By.cssSelector("#submit")).click();
        // 2.进入到游戏大厅页面后,点击匹配按钮
        driver.findElement(By.cssSelector("#match-button")).click();
        // 3.获取点击匹配后的信息
        String actual = "匹配中...(点击停止)";
        getScreenShot(getClass().getName());
        String clickAfter = driver.findElement(By.cssSelector("#match-button")).getText();
        // 4.判断
        Assertions.assertEquals(clickAfter, actual);
        // 5.再次点击
        driver.findElement(By.cssSelector("#match-button")).click();
        String clickAgain = driver.findElement(By.cssSelector("#match-button")).getText();
        // 6.判断
        actual = "开始匹配";
        Assertions.assertEquals(clickAgain, actual);
        getScreenShot(getClass().getName());
    }

(4)游戏对战页面

根据测试用例对游戏匹配成功后游戏对战页面进行测试,主要测试点是页面上的棋盘和显示屏元素。
测试步骤:
1)防止多开,创建两个浏览器驱动(edgeDriver、chromeDriver);
2)分别在两个浏览器上登录不同的账户;
3)登录成功后进入游戏大厅页面,分别点击开始匹配按钮;
4)匹配成功后进入游戏对战页面,寻找棋盘和显示屏两个标志元素。

    // 创建驱动
    private static EdgeDriver edgeDriver = createEdgeDriver();
    private static ChromeDriver chromeDriver = createChromeDriver();

    // 将两个不同系统的用户匹配进同一游戏房间
    public void userLogin() {
        // user1
        edgeDriver.get("http://127.0.0.1:8081/login.html");
        edgeDriver.findElement(By.cssSelector("#username")).sendKeys("六1");
        edgeDriver.findElement(By.cssSelector("#password")).sendKeys("123");
        edgeDriver.findElement(By.cssSelector("#submit")).click();
        // user2
        chromeDriver.get("http://127.0.0.1:8081/login.html");
        chromeDriver.findElement(By.cssSelector("#username")).sendKeys("王五");
        chromeDriver.findElement(By.cssSelector("#password")).sendKeys("123");
        chromeDriver.findElement(By.cssSelector("#submit")).click();
        // user1和user2进同一游戏房间
        edgeDriver.findElement(By.cssSelector("#match-button")).click();
        chromeDriver.findElement(By.cssSelector("#match-button")).click();
    }

    /**
     * 测试游戏对战页面
     * 检查点:有棋盘、提示版元素
     */
    @Test
    @Order(1)
    public void testGameBoard() {
        // 不同系统的两个用户匹配到同一个房间
        userLogin();
        // 测试游戏对战页面
        edgeDriver.findElement(By.cssSelector("#chess"));
        chromeDriver.findElement(By.cssSelector("#chess"));
    }

4.项目测试的结果

视频演示链接

在这里插入图片描述


完整项目代码可联系博主。

总结

这篇文章记录了五子棋项目现在已实现的功能及web页面自动化测试,后续将对该项目扩充聊天功能(同一个房间的用户可以发送消息)及测试方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1580411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT与生成式AI:教育领域内新的浪潮与挑战

随着ChatGPT和其他生成式AI技术&#xff0c;如GPT-3.5、GPT-4的出现&#xff0c;我们正见证教育领域一场前所未有的变革浪潮。这些技术不仅推动了教育方式的进步&#xff0c;也为学习者带来了全新的机遇和挑战。 NO.1教育变革的新浪潮 生成式AI技术&#xff0c;特别是ChatGPT&…

【Nature Electronics】二维钙钛矿氧化物SNO作为high-κ栅介质的应用

【Li, S., Liu, X., Yang, H. et al. Two-dimensional perovskite oxide as a photoactive high-κ gate dielectric. Nat Electron 7, 216–224 (2024). https://doi.org/10.1038/s41928-024-01129-9】 概括总结&#xff1a; 本研究探讨了二维钙钛矿氧化物Sr2Nb3O10&#xf…

我们有了统一的Domino应用市场

大家好&#xff0c;才是真的好。 和大家一样&#xff0c;刚休假回来就发现HCL又上新了。这回是个大消息&#xff0c;直接上了一个Notes/Domino应用市场&#xff0c;地址是https://hclsofy.com/domino。 截至本篇写作时&#xff0c;提交到该市场的Notes/Domino相关扩展工具和应…

ATM04-6P 安费诺汽车连接器6芯压线端子胶壳

ATM04-6P是一款压线端子胶壳&#xff0c;属于Amphenol&#xff08;安费诺&#xff09;品牌 ATM04-6P 规格信息&#xff1a; 制造商:Amphenol 产品种类:汽车连接器 RoHS:是 产品:Connectors 位置数量:6 Position 型式:Receptacle (Female) 线规量程:22 AWG to 16 AWG 系列:ATM 颜…

layui后台框架,将左侧功能栏目 集中到一个页面,通过上面的tab切换 在iframe加载对应页面

实现上面的 功能效果。 1 html代码 <form class"layui-form layui-form-pane" action""><div class"layui-tab" lay-filter"demo"><ul class"layui-tab-title"><li id"a0" class"lay…

常见的mq产品和优点

常见的mq产品和优点 一、什么是mq? MQ全称 Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信&#xff0c;解耦。 二、常见的mq产品 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq …

基于51单片机轮胎胎压监测系统—数码管显示

基于51单片机轮胎胎压监测系统 &#xff08;仿真&#xff0b;程序&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 1.MPX4115压力传感器胎压检测&#xff1b; 2.ADC0832进行模数转换后&#xff0c;51单片机处理控制&#xff1b; 3.数码管显示胎压&#xff…

谷歌留痕霸屏要怎么做?

谷歌留痕霸屏&#xff0c;就是让你的网站或者页面在谷歌搜索结果里尽可能多地出现&#xff0c;就像是在你的潜在客户眼前留下深刻印象一样&#xff0c;你要做的就是在一些高权重平台发布有价值的信息&#xff0c;同时巧妙地留下你的品牌名、产品名或者任何你想要推广的关键词&a…

Selenium+Chrome Driver 爬取搜狐页面信息

进行selenium包和chromedriver驱动的安装 安装selenium包 在命令行或者anaconda prompt 中输入 pip install Selenium 安装 chromedriver 先查看chrome浏览器的版本 这里是 123.0.6312.106 版 然后在http://npm.taobao.org/mirrors/chromedriver/或者https://googlechrom…

位图/矢量图/GIF/PNG/JPEG/WEBP一网打尽

❝ 想把一件事做好&#xff0c;那就先把眼前的小事攻克 ❞ 大家好&#xff0c;我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder。 前言 最近&#xff0c;在做项目资源打包优化。如果大家做过类似的工作&#xff0c;在操作过程中&#xff0c;想必有一个…

实战:gcc-11.4.0编译wxWidgets-2.8.12的xrc演示例程

由于我上次编译安装的wxWidgets-2.8.12的abi是1009的&#xff0c;现在编译xrc演示程序也要使用09的abi才能正常运行。 到sourceforge去下载wxGTK-2.8.12&#xff0c;然后解压。 上述的文件夹中有msvc的make, 有watcom的makefile, 还有 unx 的makefile, 在mint-21.3上就用unx这…

配置VM开机自启动

1. 在此电脑-右键选择“管理”-服务和应用程序-服务中找到VMware Workstation Server服务&#xff08;新版名称也可能是VMware自启动服务&#xff0c;自己找一下&#xff0c;服务属性里有描述信息的&#xff09;&#xff0c;将其启用并选择开机自动启动 新版参考官方文档&…

抖音变现项目有哪些?来这几个资源网站看看吧

做短视频就像是在做一道菜&#xff0c;你得有那么几个秘密武器&#xff0c;才能让你的作品从众多视频中脱颖而出。我这个视频剪辑界的“烹饪大师”&#xff0c;今天就来给大家分享一下我的厨房秘籍——那些让我视频大放异彩的素材网站。九才素材网&#xff1a; 说起九才素材网…

吴恩达深度学习笔记:深层神经网络(Deep Neural Networks)4.1-4.4

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第四周&#xff1a;深层神经网络(Deep Neural Networks)4.1 深层神经网络&#xff08;Deep L-layer neural network&#xff09;4.2 前向传播和反向传播&#xff08;Forward and backward pro…

3dmax渲染十几个小时怎么办?3dmax怎么多机渲染

当使用3ds Max进行渲染作业时&#xff0c;如果发现单张图像的渲染时间长达十数小时&#xff0c;这可能是由于计算机硬件配置较低或渲染场景过于复杂所致。为了缩短渲染时间并提高效率&#xff0c;我们可以考虑采用多台计算机进行协同渲染。下面&#xff0c;让我们一起探讨如何通…

Webots常用的执行器(Python版)

文章目录 1. RotationalMotor2. LinearMotor3. Brake4. Propeller5. Pen6. LED 1. RotationalMotor # -*- coding: utf-8 -*- """motor_controller controller."""from controller import Robot# 实例化机器人 robot Robot()# 获取基本仿真步长…

【编译原理】Antlr 入门使用

前面文章我们学习了编译器前端的词法和语法分析工具&#xff0c;本篇我们来看看如何借助 Antlr 工具&#xff0c;快速生成词法和语法分析代码。 一、安装 mac 环境&#xff1a; 1&#xff09;安装 brew install antlr2&#xff09;配置 classpath &#xff08;把 Antlr 的 J…

MySQL -- 07_最流行的查询需求分析(一些分组排序查询、开窗函数 dense_rank、distinct 去重函数 等~)

目录 最流行的查询需求分析07演示数据准备的SQL需求演示36、查询每一门课程成绩都在70分以上的姓名、课程名称和分数group by min() in() 函数 37、查询不及格的课程及学生普通表连接查询 38、查询课程编号为01语文且课程成绩在80分以上的学生的学号和姓名普通表连接查询 39、…

使用axios进行前后端数据传输

最近在和朋友合作写一个新的项目&#xff0c;前后端进行了分离&#xff0c;既然是分离的&#xff0c;肯定需要交互&#xff0c;今天这篇文章详细介绍一下数据交互的一种常见方式&#xff1a;使用axios,希望对大家有所帮助。 前端&#xff1a;以LoginPage.vue登录页面为例&…

扬帆出海扩规模,仍是比亚迪未来的发展关键?

又到了新能源车企公布阶段性成果的时期。 日前&#xff0c;乘联会预估2024年3月全国新能源乘用车厂商批发销量82万辆&#xff0c;同比增长33%&#xff0c;环比增长84%。其中&#xff0c;比亚迪继续领跑&#xff0c;3月销量超30万辆&#xff0c;环比增长147.8%&#xff0c;而这…