企业级应用如何用 Apache DolphinScheduler 有针对性地进行告警插件开发?

news2025/1/11 0:26:04

点击蓝字 关注我们

a22c9d8bdde9db52666e2d6a33b42d4e.jpeg

作者 | 刘宇星

Apache DolphinScheduler的2.0.1版本加入了插件化架构改进,将任务、告警组件、数据源、资源存储、注册中心等都将被设计为扩展点,以此来提高 Apache DolphinScheduler 本身的灵活性和友好性。在企业级应用中不同公司的告警需求可能各有不同,针对性的告警插件开发可以很好地解决这一痛点。

版本:3.1.2

告警插件开发

先来看下alert目录的结构

91bdbf1e7c547ca06fcebb4a5855ba4f.png

  • dolphinscheduler-alert-api

  • 该模块是 ALERT SPI 的核心模块,该模块定义了告警插件扩展的接口以及一些基础代码,其中 AlertChannel 和 AlertChannelFactory 是告警插件开发需要实现的接口类

  • dolphinscheduler-alert-plugins

  • 该模块包含了官方提供的告警插件,目前我们已经支持数十种插件,如 Email、DingTalk、Script等

  • dolphinscheduler-alert-server

  • 告警服务模块,主要功能包括注册告警插件,Netty告警消息发送等

本文以官方的http告警插件为例讲解如何进行插件开发

  • 首先明确需求,http告警插件需要通过http发送请求,发送请求首先需要确定哪些参数.在 HttpAlertConstants 可以看到有定义一些相关参数

package org.apache.dolphinscheduler.plugin.alert.http;
public final class HttpAlertConstants {
    public static final String URL = "$t('url')";

    public static final String NAME_URL = "url";

    public static final String HEADER_PARAMS = "$t('headerParams')";

    public static final String NAME_HEADER_PARAMS = "headerParams";

...........................省略多余代码

    private HttpAlertConstants() {
        throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
    }
}
  • 对应此处告警实例需要填写的参数

529872f87e0c81969cb4efa2f33ba495.png

其中 $t('url') 样式的参数可以通过编辑

dolphinscheduler-ui/src/locales/zh_CN/security.ts

添加对应的参数,前端收到后会自动替换,同样的英文字典也需要替换,不然切换英文时会报错

  • HttpAlertChannelFactory需要实现AlertChannelFactory并实现它的方法name,paramscreate。其中InputParam.newBuilder的第一个参数是显示的值,第二个参数是参数名,这里用我们前面在MailParamsConstants写好的常量。所有参数写好后添加到paramsList后返回

@AutoService(AlertChannelFactory.class)
public final class HttpAlertChannelFactory implements AlertChannelFactory {
    @Override
    public String name() {
        return "Http";
    }
    @Override
    public List<PluginParams> params() {
        InputParam url = InputParam.newBuilder(HttpAlertConstants.NAME_URL, HttpAlertConstants.URL)
                                   .setPlaceholder("input request URL")
                                   .addValidate(Validate.newBuilder()
                                                        .setRequired(true)
                                                        .build())
                                   .build();
        InputParam headerParams = InputParam.newBuilder(HttpAlertConstants.NAME_HEADER_PARAMS, HttpAlertConstants.HEADER_PARAMS)
                                            .setPlaceholder("input request headers as JSON format ")
                                            .addValidate(Validate.newBuilder()
                                                                 .setRequired(true)
                                                                 .build())
                                            .build();
        InputParam bodyParams = InputParam.newBuilder(HttpAlertConstants.NAME_BODY_PARAMS, HttpAlertConstants.BODY_PARAMS)
                                          .setPlaceholder("input request body as JSON format ")
                                          .addValidate(Validate.newBuilder()
                                                               .setRequired(false)
                                                               .build())
                                          .build();
...........................省略多余代码
        return Arrays.asList(url, requestType, headerParams, bodyParams, contentField);
    }
    @Override
    public AlertChannel create() {
        return new HttpAlertChannel();
    }
}
  • HttpAlertChannel需要实现AlertChannel并实现process方法,其中alertInfo.getAlertData().getAlertParams()可以拿到在创建告警实例时填写的参数,在此处编写相关代码发送请求后,需要返回AlertResult对象用来标记请求发送or失败

public final class HttpAlertChannel implements AlertChannel {
    @Override
    public AlertResult process(AlertInfo alertInfo) {
        AlertData alertData = alertInfo.getAlertData();
        Map<String, String> paramsMap = alertInfo.getAlertParams();
        if (null == paramsMap) {
            return new AlertResult("false", "http params is null");
        }
        return new HttpSender(paramsMap).send(alertData.getContent());
    }
}

至此插件开发就完成的,是不是很简单:)设计优秀架构合理的代码就应该是这样优雅高效解耦合. 完成以上开发后,启动告警服务,就可以在添加告警实例时选择对应的插件了。

2f1b6a6ef8808425ef82b862a67b713c.png

源码解读

在启动告警服务时,可以在日志看到有注册告警插件的信息

5db3332a791ae62a815448b3c89f757f.png

以此为切入口来探索插件实现的相关代码

  • 在dolphinscheduler-alert-server的AlertPluginManager的 installPlugin 方法可以看到注册告警插件的内容,这里先获取所有实现了AlertChannelFactory.class的类,遍历后获取AlertChannel的实例,添加到数据库和channelKeyedByIdMap

private final Map<Integer, AlertChannel> channelKeyedById = new HashMap<>();
    
    @EventListener
    public void installPlugin(ApplicationReadyEvent readyEvent) {
        PrioritySPIFactory<AlertChannelFactory> prioritySPIFactory = new PrioritySPIFactory<>(AlertChannelFactory.class);
        for (Map.Entry<String, AlertChannelFactory> entry : prioritySPIFactory.getSPIMap().entrySet()) {
            String name = entry.getKey();
            AlertChannelFactory factory = entry.getValue();
            logger.info("Registering alert plugin: {} - {}", name, factory.getClass());
            final AlertChannel alertChannel = factory.create();
            logger.info("Registered alert plugin: {} - {}", name, factory.getClass());
            final List<PluginParams> params = new ArrayList<>(factory.params());
            params.add(0, warningTypeParams);
            final String paramsJson = PluginParamsTransfer.transferParamsToJson(params);
            final PluginDefine pluginDefine = new PluginDefine(name, PluginType.ALERT.getDesc(), paramsJson);
            final int id = pluginDao.addOrUpdatePluginDefine(pluginDefine);
            channelKeyedById.put(id, alertChannel);
        }
    }
  • 完成插件的开发和注册后,需要有个轮询线程来遍历查询需要发送的消息和完成发送的动作,在AlertSenderServicerun方法完成了这些

@Override
public void run() {
    logger.info("alert sender started");
    while (!ServerLifeCycleManager.isStopped()) {
        try {
            List<Alert> alerts = alertDao.listPendingAlerts();
            AlertServerMetrics.registerPendingAlertGauge(alerts::size);
            this.send(alerts);
            ThreadUtils.sleep(Constants.SLEEP_TIME_MILLIS * 5L);
        } catch (Exception e) {
            logger.error("alert sender thread error", e);
        }
    }
}
  • 关键方法是this.send(alerts),这里遍历Alert后获取告警插件的实例集合,在 this.alertResultHandler(instance, alertData)传入插件实例对象和告警参数,最后更新这条告警消息的状态

public void send(List<Alert> alerts) {
    for (Alert alert : alerts) {
        // get alert group from alert
        int alertId = Optional.ofNullable(alert.getId()).orElse(0);
        int alertGroupId = Optional.ofNullable(alert.getAlertGroupId()).orElse(0);
        List<AlertPluginInstance> alertInstanceList = alertDao.listInstanceByAlertGroupId(alertGroupId);
        if (CollectionUtils.isEmpty(alertInstanceList)) {
            logger.error("send alert msg fail,no bind plugin instance.");
            List<AlertResult> alertResults = Lists.newArrayList(new AlertResult("false",
                    "no bind plugin instance"));
            alertDao.updateAlert(AlertStatus.EXECUTION_FAILURE, JSONUtils.toJsonString(alertResults), alertId);
            continue;
        }
        AlertData alertData = AlertData.builder()
                .id(alertId)
                .content(alert.getContent())
                .log(alert.getLog())
                .title(alert.getTitle())
                .warnType(alert.getWarningType().getCode())
                .alertType(alert.getAlertType().getCode())
                .build();

        int sendSuccessCount = 0;
        List<AlertResult> alertResults = new ArrayList<>();
        for (AlertPluginInstance instance : alertInstanceList) {
            AlertResult alertResult = this.alertResultHandler(instance, alertData);
            if (alertResult != null) {
                AlertStatus sendStatus = Boolean.parseBoolean(String.valueOf(alertResult.getStatus()))
                        ? AlertStatus.EXECUTION_SUCCESS
                        : AlertStatus.EXECUTION_FAILURE;
                alertDao.addAlertSendStatus(sendStatus, JSONUtils.toJsonString(alertResult), alertId,
                        instance.getId());
                if (sendStatus.equals(AlertStatus.EXECUTION_SUCCESS)) {
                    sendSuccessCount++;
                    AlertServerMetrics.incAlertSuccessCount();
                } else {
                    AlertServerMetrics.incAlertFailCount();
                }
                alertResults.add(alertResult);
            }
        }
        AlertStatus alertStatus = AlertStatus.EXECUTION_SUCCESS;
        if (sendSuccessCount == 0) {
            alertStatus = AlertStatus.EXECUTION_FAILURE;
        } else if (sendSuccessCount < alertInstanceList.size()) {
            alertStatus = AlertStatus.EXECUTION_PARTIAL_SUCCESS;
        }
        alertDao.updateAlert(alertStatus, JSONUtils.toJsonString(alertResults), alertId);
    }
}
  • alertResultHandleralertPluginManager.getAlertChannel(instance.getPluginDefineId())获取AlertChannel实例.还记得前面注册告警插件时往channelKeyedById里put的AlertChannel实例的动作吗?

public Optional<AlertChannel> getAlertChannel(int id) {
    return Optional.ofNullable(channelKeyedById.get(id));
}
  • 然后构建AlertInfo对象,通过CompletableFuture.supplyAsync()来异步回调执行alertChannel.process(alertInfo),用future.get()获得回调执行返回的AlertResult再return

private @Nullable AlertResult alertResultHandler(AlertPluginInstance instance, AlertData alertData) {
    String pluginInstanceName = instance.getInstanceName();
    int pluginDefineId = instance.getPluginDefineId();
    Optional<AlertChannel> alertChannelOptional = alertPluginManager.getAlertChannel(instance.getPluginDefineId());
    if (!alertChannelOptional.isPresent()) {
        String message = String.format("Alert Plugin %s send error: the channel doesn't exist, pluginDefineId: %s",
                pluginInstanceName,
                pluginDefineId);
        logger.error("Alert Plugin {} send error : not found plugin {}", pluginInstanceName, pluginDefineId);
        return new AlertResult("false", message);
    }
    AlertChannel alertChannel = alertChannelOptional.get();

    Map<String, String> paramsMap = JSONUtils.toMap(instance.getPluginInstanceParams());
    String instanceWarnType = WarningType.ALL.getDescp();

    if (paramsMap != null) {
        instanceWarnType = paramsMap.getOrDefault(AlertConstants.NAME_WARNING_TYPE, WarningType.ALL.getDescp());
    }

    WarningType warningType = WarningType.of(instanceWarnType);

    if (warningType == null) {
        String message = String.format("Alert Plugin %s send error : plugin warnType is null", pluginInstanceName);
        logger.error("Alert Plugin {} send error : plugin warnType is null", pluginInstanceName);
        return new AlertResult("false", message);
    }

    boolean sendWarning = false;
    switch (warningType) {
        case ALL:
            sendWarning = true;
            break;
        case SUCCESS:
            if (alertData.getWarnType() == WarningType.SUCCESS.getCode()) {
                sendWarning = true;
            }
            break;
        case FAILURE:
            if (alertData.getWarnType() == WarningType.FAILURE.getCode()) {
                sendWarning = true;
            }
            break;
        default:
    }

    if (!sendWarning) {
        logger.info(
                "Alert Plugin {} send ignore warning type not match: plugin warning type is {}, alert data warning type is {}",
                pluginInstanceName, warningType.getCode(), alertData.getWarnType());
        return null;
    }

    AlertInfo alertInfo = AlertInfo.builder()
            .alertData(alertData)
            .alertParams(paramsMap)
            .alertPluginInstanceId(instance.getId())
            .build();
    int waitTimeout = alertConfig.getWaitTimeout();
    try {
        AlertResult alertResult;
        if (waitTimeout <= 0) {
            if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) {
                alertResult = alertChannel.closeAlert(alertInfo);
            } else {
                alertResult = alertChannel.process(alertInfo);
            }
        } else {
            CompletableFuture<AlertResult> future;
            if (alertData.getAlertType() == AlertType.CLOSE_ALERT.getCode()) {
                future = CompletableFuture.supplyAsync(() -> alertChannel.closeAlert(alertInfo));
            } else {
                future = CompletableFuture.supplyAsync(() -> alertChannel.process(alertInfo));
            }
            alertResult = future.get(waitTimeout, TimeUnit.MILLISECONDS);
        }
        if (alertResult == null) {
            throw new RuntimeException("Alert result cannot be null");
        }
        return alertResult;
    } catch (InterruptedException e) {
        logger.error("send alert error alert data id :{},", alertData.getId(), e);
        Thread.currentThread().interrupt();
        return new AlertResult("false", e.getMessage());
    } catch (Exception e) {
        logger.error("send alert error alert data id :{},", alertData.getId(), e);
        return new AlertResult("false", e.getMessage());
    }
}

综上描述,可以画出注册插件和发送消息的时序图

f9607f5d5a3a277922c46c93f7c0c601.png

以上就是告警插件的主要实现代码,是不是发现源码看下来也没有发现多高深和复杂:)所以多看看源码吧,以后你也可以写出这样优秀的开源软件来贡献开源

参考链接:

[Feature] Alert Plugin Design · Issue #3049 · apache/dolphinscheduler (https://github.com/apache/dolphinscheduler/issues/3049)

alert (https://dolphinscheduler.apache.org/zh-cn/docs/latest/user_doc/contribute/backend/spi/alert.html)

参与贡献

随着国内开源的迅猛崛起,Apache DolphinScheduler 社区迎来蓬勃发展,为了做更好用、易用的调度,真诚欢迎热爱开源的伙伴加入到开源社区中来,为中国开源崛起献上一份自己的力量,让本土开源走向全球。

3e34e810c4da8aee5a1b32b930cbca4f.png

参与 DolphinScheduler 社区有非常多的参与贡献的方式,包括:

87fd62673f5f36b684f94877bcf2f43f.png

贡献第一个PR(文档、代码) 我们也希望是简单的,第一个PR用于熟悉提交的流程和社区协作以及感受社区的友好度。

社区汇总了以下适合新手的问题列表(Good First Issue):https://github.com/apache/dolphinscheduler/contribute

非新手问题列表:https://github.com/apache/dolphinscheduler/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+q=is%3Aopen+is%3Aissue+label%3A%22volunteer+wanted%22

如何参与贡献链接:https://github.com/apache/dolphinscheduler/blob/8944fdc62295883b0fa46b137ba8aee4fde9711a/docs/docs/en/contribute/join/contribute.md

来吧,DolphinScheduler开源社区需要您的参与,为中国开源崛起添砖加瓦吧,哪怕只是小小的一块瓦,汇聚起来的力量也是巨大的。

参与开源可以近距离与各路高手切磋,迅速提升自己的技能,如果您想参与贡献,我们有个贡献者种子孵化群,可以添加社区小助手微信(Leonard-ds) ,手把手教会您( 贡献者不分水平高低,有问必答,关键是有一颗愿意贡献的心 )。

ae3125f751462ed21d627b47e64079a0.jpeg

添加社区小助手微信(Leonard-ds) 

添加小助手微信时请说明想参与贡献。

来吧,开源社区非常期待您的参与。

< 🐬🐬 >

精彩活动推荐

汽车行业走在了数字化革命浪潮的前列。大数据和 AI 技术的日益成熟,让汽车行业面对着动辄上百万的日活数据,二调度系统助力汽车数字化平台数据调度重塑着未来汽车的面貌,其重要作用不言而喻。

Apache DolphinScheduler 作为国内外多家知名车企数据平台的核心调度系统,它是如何帮助车企迎接数字化时代新挑战的?如何辅助重塑未来汽车的新面貌?欢迎大家关注即将到来的 Apache DolphinScheduler 汽车行业最佳应用实践专场直播

直播时间:倒计时 1 小时!2023 年 5 月 23 日 19:00-21:00

预约方式:点击预约,视频号直播不见不散!

点击阅读原文,点亮Star支持我们哟

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

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

相关文章

ChatGPT被广泛应用,潜在的法律风险有哪些?

ChatGPT由OpenAI开发&#xff0c;2022年11月一经面世便引发热烈讨论&#xff0c;用户数持续暴涨。2023年初&#xff0c;微软成功将ChatGPT接入其搜索引擎Bing中&#xff0c;市场影响力迅速提升&#xff0c;几乎同一时间&#xff0c;谷歌宣布其研发的一款类似人工智能应用Bard上…

树莓派 Ubuntu 18.04 连接 WiFi

树莓派 Ubuntu 18.04 连接 WiFi 阿瑞特后视镜那边代码调试需要用到树莓派&#xff0c;但是实验室 TP-LINK-DD48 用不了 所以要更改原先的 WiFi 连接信息 树莓派raspberry Pi 4B安装Ubuntu 20.04 LTS系统后如何连接WiFi 树莓派4B(ubuntu)设置wifi的方法 树莓派4B安装Ubuntu Se…

函数式接口入门简介(存在疑问,求解答)

这里写目录标题 引子四种函数式接口-简单Demo四种函数式接口介绍函数式接口实战-代码对比关于Consumer赋值问题&#xff08;疑问&#xff0c;求解答&#xff09; 引子 只包含一个抽象方法的接口&#xff0c;就称为函数式接口。来源&#xff1a;java.util.function 我想在方法…

【JS】1691- 重学 JavaScript API - Performance API

❝ 前期回顾&#xff1a; 1. Page Visibility API 2. Broadcast Channel API 3. Beacon API 4. Resize Observer API 5. Clipboard API 6. Fetch API ❞ &#x1f3dd; 1. 什么是 Performance API 1.1 概念介绍 Performance API 提供了「访问和测量浏览器性能相关信息」的方法。…

作为IT行业过来人,我有4个重要建议给年轻程序员!

见字如面&#xff0c;我是军哥&#xff01; 作为一名 40 岁的 IT 老兵&#xff0c;我在年轻时踩了不少坑&#xff0c;至少有两打&#xff0c;我总结了其中最重要的 4 个并一次性分享给你&#xff0c;文章不长&#xff0c;你一定要看完哈&#xff5e; 1、重视基础还不够&#xf…

OpenAI Whisper + FFmpeg + TTS:动态实现跨语言视频音频翻译

本文作者系360奇舞团前端开发工程师 摘要&#xff1a; 本文介绍了如何结合 OpenAI Whisper、FFmpeg 和 TTS&#xff08;Text-to-Speech&#xff09;技术&#xff0c;以实现将视频翻译为其他语言并更换声音的过程。我们将探讨如何使用 OpenAI Whisper 进行语音识别和翻译&#x…

软件设计师 操作系统涉及题目

做题技巧 看有几个箭头就是有几个信号量。比如四个箭头就是S1 S2 S3 4把对应的信号量从小到大顺序放在对应箭头 比如P1-》P2就是 12 P1-》P3就是13 所以13大 注意是先V(S) 再P(S)&#xff0c;箭头前是v后是p **P1没有前驱&#xff0c;第一个执行的进程.执行前用P操作 执行后用…

媒体传输协议的演进与未来

音视频应用近年来呈现出迅猛的发展趋势&#xff0c;成为互联网流量的主要载体&#xff0c;其玩法丰富&#xff0c;形态多样&#xff0c;众多繁杂的媒体传输协议也应运而生。LiveVideoStackCon 2022北京站邀请到快手传输算法负责人周超&#xff0c;结合快手在媒体传输上的优化与…

官宣!首个大模型兴趣小组开放申请,专注大模型应用落地

‍‍ 这里汇聚着大模型开发者与应用者 这是一个小而美小而精的兴趣组织 这是一个更关注大模型行业实际落地的组织 飞桨 AI Studio 大模型领域兴趣小组关注文心一言等大模型与开源模型应用落地&#xff0c;跟进最新技术趋势与应用方向&#xff0c;共同拓展技术视野、找寻商业化机…

QSS QTableWidget样式设置

QTableWidget的样式分为几个部分&#xff1a; 分别是&#xff1a; 外框&#xff1a;QTableWidget 表头&#xff1a;QHeaderView 表头字段&#xff1a;QHeaderView::section 表格&#xff1a;QTableWidget::item 选中的表格&#xff1a;QTableWidget::item::selected 水平滚动条…

chatgpt赋能Python-python_pensize

Python Pensize: How to Adjust Your Pen Size in Python If you’re new to Python, you might be struggling to master the art of the pen. Thankfully, Python Pensize is here to help. In this article, we’ll discuss how to adjust your pen size in Python so you …

快手广告怎么顺利度过冷启动期?

快手广告经常会出现这样的问题&#xff0c;投放初期新广告主、新产品、新账户都很难拿到曝光&#xff0c;没法突破&#xff1b;今天给大家介绍下什么是冷启动&#xff0c;如何快速有效的度过冷启动期。 冷启动就是刚开始启动的时候没有基础&#xff0c;模型需要根据历史情况来预…

Linux 查看或统计网卡流量的几种方式么?

在工作中&#xff0c;我们经常需要查看服务器的实时网卡流量。通常&#xff0c;我们会通过这几种方式查看Linux服务器的实时网卡流量。 目录 1、sar 2、 /proc/net/dev 3、ifstat 4、iftop 5、nload 6、iptraf-ng 7、nethogs 8、扩展 1、sar sar命令包含在sysstat工具…

大Op和小op的含义及理解

大Op和小op的含义及理解 Stochastic order notation(随机有序符号)6.1.1 O p O_p Op​和 o p o_p op​之间的关系6.2 符号速记及其算数性质6.3 为什么 o p o_p op​和 O p O_p Op​符号很有用&#xff1f;6.4例子&#xff1a;均值估计的相合性参考&#xff1a; Stochastic orde…

基于大模型GPT,如何提炼出优质的Prompt

基于大模型实现优质Prompt开发 1. 引言1.1 大规模预训练模型 2. Prompt开发2.1 Prompt基本定义&#xff1a;2.2 为什么优质Prompt才能生成优质的内容2.3 如何定义优质的Prompt 3. Prompt优化技巧3.1 迭代法3.1.1 创作评估3.1.2 基础创作3.1.3 多轮次交互 3.2 Trick法3.2.1 戴高…

【MySQL入门实战4】-发行版本及安装概述

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

python+django基于爬虫系统的世界历史时间轴历史事件大事记6ouj9

随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&#xf…

chatgpt赋能Python-python_pyuserinput

介绍 Python是一种极其流行的编程语言&#xff0c;可以用于多种任务&#xff0c;例如数据分析、机器学习、Web开发等等。 Python社区非常活跃&#xff0c;因此有许多模块和库可以用于各种任务&#xff0c;包括用户输入和自动化。 PyUserInput是一个Python库&#xff0c;它提供…

C#,码海拾贝(22)——线性方程组求解的全选主元高斯-约当消去法之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> …

小航编程题库机器人等级考试理论一级(2022年9月) (含题库教师学生账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSDN博客 单选题2.0分 删除编辑 答案:C 第1题使用下列工具不能省力的是&#xff1f;&#xff08; &#xff09; A、斜面B、动滑轮C、定滑轮D…