如何优雅的实现Excel导入通用处理流程

news2024/11/15 15:34:40

目录

    • 1.业务背景
    • 2.业务导入流程
    • 3.流程优化
      • 3.1 模板模式
        • 3.1.1 导入处理器接口`ImportProcessor`
        • 3.1.2 抽象父类 `AbstractImportProcessor`
        • 3.1.3 子类实现 `ImportDemoProcessor`
      • 3.2 工厂模式
        • 3.2.1 标识子类的枚举`ImportTypeEnum`
        • 3.2.2 工厂类`ProcessorHolder`
        • 3.2.3 工厂类的调用
    • 4. 特别关注的点
      • 4.1 EasyExcel的使用
        • 4.1.1. ReadListener
        • 4.1.2 Excel实体字段类型转换问题
      • 4.2 规则引擎的使用
    • 总结

1.业务背景

对于一个内部使用的交付系统来说,业务场景大多需要录入数据,时常的导入Excel业务数据是常态,交付人员通过批量的导入业务数据替代页面表单录入,可大幅提升工作效率。

业务的多样性往往也会伴随着个性化的导入Excel逻辑,例如我们的业务系统来说,导入数据场景是最常见的case,而且随着业务的不断发展,场景也在逐步增加。

导入财务数据导入人员数据导入众包订单数据导入外包人员发薪数据等等。导入完毕后需要数据进行校验,然后回显校验结果。

在这里插入图片描述

2.业务导入流程

对于我们的业务系统来说,导入数据一般需要以下的步骤:

  1. 解析Excel
  2. 校验Excel
  3. 展示校验结果
  4. 校验成功后确认导入数据

还需要两类表模型:

1.导入数据的临时表

数据可以多次导入,然后进行校验,只有校验成功的数据才能进入下一步。

2.导入数据的实际业务数据表

导入校验成功后,生成业务数据,如果导入数据有唯一编码,业务数据表只会生成一条

每种业务场景如果都单独写一遍导入流程,再单独建立临时表的话,对于程序员来说无疑是一种折磨。

如何将上述场景流程通用化,当新增导入数据场景时,可以忽略通用部分,只需要聚焦于实际的业务逻辑呢?


3.流程优化

对于复杂的业务场景,如果要做到模块化、低耦合,我理解大部分时候还是需要围绕封装抽象来进行。

封装不变部分抽象不变部分来用于后续的拓展

就本文讨论的业务场景,核心不变的部分是整体的导入流程,无论业务场景怎么多变,数据进入到系统中的如下步骤是固定的。

  1. 解析Excel
  2. 校验Excel
  3. 展示校验结果
  4. 校验成功后确认导入数据

3.1 模板模式

设计模式中模板模式无疑是解决重复代码的一大利器,应用在当前场景再合适不过了。

主要实现

在父类中定义了算法的骨架,并允许子类在不改变算法结构的前提下重定义算法的某些特定步骤。

设计意图

解决在多个子类中重复实现相同的方法的问题,通过将通用方法抽象到父类中来避免代码重复。

整体代码
在这里插入图片描述

通用的临时表模型
所有导入的数据在解析校验后都会存储到此表中,不同的业务实体转为json后存储在Import_data字段中。
不同的业务表需要根据相应的业务进行创建,此处就不进行展示了。

create table import_data_tmp
(
    id                 bigserial
        primary key,
    batch_id           varchar(50)              not null,
    operator_id        bigint                   not null,
    operation_time     timestamp with time zone not null,
    line_num           bigint,
    import_data        text                     not null,
    import_result      integer,
    import_description varchar(500),
    create_time        timestamp with time zone not null,
    update_time        timestamp with time zone not null
);

comment on table import_data_tmp is '导入信息临时表';

comment on column import_data_tmp.id is '主键ID';

comment on column import_data_tmp.batch_id is '批次号';

comment on column import_data_tmp.operator_id is '操作人Id';

comment on column import_data_tmp.operation_time is '操作时间';

comment on column import_data_tmp.line_num is '导入文件行号';

comment on column import_data_tmp.import_data is '导入数据';

comment on column import_data_tmp.import_result is '导入结果';

comment on column import_data_tmp.import_description is '导入描述';

comment on column import_data_tmp.create_time is '创建时间';

comment on column import_data_tmp.update_time is '更新时间';

create index import_data_tmp_batch_id
    on import_data_tmp (batch_id);

create index import_data_tmp_operator_id
    on import_data_tmp (operator_id);

3.1.1 导入处理器接口ImportProcessor

ImportProcessor在此类中,定义抽象父类方法的处理函数ImportResultDTO process(String url, R r);定义一些子类需要自己实现的方法(这些方法也可在抽象父类中定义)。

public interface ImportProcessor<T, R> {

    /**
     * 核心导入处理流程
     * @param url 导入的excel地址
     * @param r 请求入参数据
     * @return 处理结果
     */
    ImportResultDTO process(String url, R r);

    /**
     * 生成批次号
     * @return 批次号
     */
    String createBatchId();

    /**
     * 读取excel数据
     * @param r – 请求入参数据
     * @return 解析出的数据
     */
    List<T> readData(R r);

    /**
     * 组装需要存储的实体
     * @param dataList 解析出的数据
     * @param r 请求入参数据
     * @return 需要存储的实体
     */
    List<ImportDataTmpDO> assembleData(List<T> dataList, R r);

    /**
     * 导入成功的数据,生成真正的业务数据
     * @param batchId 成功的批次号
     * @return 导入结果
     */
    boolean doImportSuccessData(String batchId);
}


3.1.2 抽象父类 AbstractImportProcessor

核心类AbstractImportProcessor,process方法定义算法骨架,即导入流程

在这里插入图片描述

根据不同的业务,子类需要实现的方法。

整体代码如下:

@Component
@Slf4j
public abstract class AbstractImportProcessor<T, R> implements ImportProcessor<T, R> {
    @Resource
    private ImportDataTmpGateway importGateway;
    @Resource
    protected DistributeIDGenerator idGenerator;
    @Resource
    protected RuleEngineCmdExe ruleEngine;

    /**
     * 导入类型
     */
    protected ImportTypeEnum importType;
    /**
     * 解析出的数据
     */
    protected final ThreadLocal<List<T>> dataList = ThreadLocal.withInitial(ArrayList::new);
    /**
     * 导入结果
     */
    protected final ThreadLocal<ImportResultDTO> result = ThreadLocal.withInitial(ImportResultDTO::new);
    /**
     * 批次号
     */
    protected final ThreadLocal<String> batchId = new ThreadLocal<>();

    @Override
    public ImportResultDTO process(String url, R r) {
        try {
            //生成批次号
            String batchId = createBatchId();
            this.batchId.set(batchId);
            result.get().setBatchId(batchId);

            //解析读取数据
            dataList.set(readData(r));

            //组装数据
            List<ImportDataTmpDO> temps = assembleData(dataList.get(), r);

            //存储数据
            saveBatch(temps);

            return result.get();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("导入异常:{}", e);
            result.get().setImportResult(ImportDataConstants.IMPORT_RESULT_FAILED);
            result.get().setImportDescription(e.getMessage());
            return result.get();
        } finally {
            dataList.remove();
            result.remove();
            batchId.remove();
        }
    }

    @Override
    public List<T> readData(R r) {
        return readDataFromExcel(r);
    }

    /**
     * 组装通用的临时数据表
     * @param data 需要转换为json的实体
     * @param gigUserId 用户id
     * @param batchId 批次编号
     * @param lineNum excel行号
     * @return 组装完毕的实体
     */
    protected ImportDataTmpDO assembleTmp(T data, Long gigUserId, String batchId, Long lineNum) {
        ImportDataTmpDO tmp = new ImportDataTmpDO();
        tmp.setBatchId(batchId);
        tmp.setLineNum(lineNum);
        tmp.setOperatorId(gigUserId);
        tmp.setOperationTime(Instant.now());
        tmp.setImportData(JSONObject.toJSONString(data));
        tmp.setImportDescription("");
        return tmp;
    }

    /**
     * 使用自定义规则引擎校验数据
     * @param context 上下文数据
     * @param temp 组装后的数据
     */
    protected void checkData(RuleContext<T> context, ImportDataTmpDO temp) {
        RuleExecuteResult result = this.ruleEngine.executeRule(context);
        temp.setImportResult(ImportDataConstants.IMPORT_RESULT_SUCCESS);
        if (result.isRefuse()) {
            //拼接校验提示语
            temp.setImportResult(ImportDataConstants.IMPORT_RESULT_FAILED);
            temp.setImportDescription((temp.getImportDescription() == null ? "" : temp.getImportDescription()) + result.getHitRules().stream().reduce("",(a, b) -> a + b + ";"));
        }
    }

    /**
     * 保存数据至通用临时表中
     * @param temps 组装后的数据
     */
    private void saveBatch(List<ImportDataTmpDO> temps) {
        this.importGateway.saveImportTaskTmpBatch(temps);
    }

    /**
     * 读取数据
     * @param r 数据
     * @return 解析校验后的数据
     */
    public abstract List<T> readDataFromExcel(R r);

    /**
     * 获取数据流
     * @param fileUrl 文件存储地址
     * @return 数据流
     */
    protected InputStream getInputStream(String fileUrl) {
        StreamResponseHolder responseHolder = HttpUtils.queryAsStream(HttpUtils.createGet(fileUrl));
        return responseHolder.getValue();
    }

    /**
     * 初始化不同的导入处理器到工厂中
     */
    @PostConstruct
    protected void init() {
        ProcessorHolder.putProcessor(importType, this);
    }
}

3.1.3 子类实现 ImportDemoProcessor

当新增导入数据场景时,只需要新增一个子类,例如ImportDemoProcessor,继承AbstractImportProcessor,实现相应的方法即可,如下。
在这里插入图片描述

完善后的子类如下。核心实现有两个:
1.从excel中读取数据readDataFromExcel,可以自定义一个DemoReadListener来处理excel解析,一般不包含业务逻辑。
这里借助EasyExcel框架,具体使用可以查看官网https://easyexcel.opensource.alibaba.com/

2.填充业务实体ImportDemoDOassembleData()方法,其中可以自定义业务逻辑,用于完善字段,比如业务逻辑的校验,可以调用自定义的规则引擎进行校验,规则引擎也是一个单独实现的模块,具体可查看之前我写过的一篇文章。
基于Aviator开发一个简单的规则引擎

@Data
public class ImportDemoDO {

    @ExcelProperty(index = 0)
    private String userName;

    @ExcelProperty(index = 1)
    private String address;

    @ExcelProperty(index = 2, converter = BigDecimalConverter.class)
    private BigDecimal salary;

    /**
     * 行号 用于前端的展示
     */
    private Long lineNum;
}
public class ImportDemoProcessor extends AbstractImportProcessor<ImportDemoDO, ImportFileUrlRequest> {

    /**
     * 每次导入的批次号前缀
     */
    private static final String IMPORT_PREFIX = "DEMO";

    /**
     * 导入excel的表头列数限制
     */
    private static final Integer HEAD_SIZE = 6;

    /**
     * 具体的业务表基础层处理
     */
    private final DemoGateway demoGateway;
    private final ImportDataTmpGateway tempGateway;

    public ImportDemoProcessor(DemoGateway demoGateway, ImportDataTmpGateway tempGateway) {
        this.demoGateway = demoGateway;
        this.tempGateway = tempGateway;
        //定义一个业务导入类型ImportTypeEnum.DEMO,会在父类的init方法中添加进导入类型工厂中
        this.importType = ImportTypeEnum.DEMO;
    }

    @Override
    public List<ImportDemoDO> readDataFromExcel(ImportFileUrlRequest request) {
        //使用EasyExcel读取数据,其中DemoReadListener可以对读取解析过程自定义一些处理
        return EasyExcel.read(this.getInputStream(request.getUrl()),
                ImportBillTypeADO.class,
                new DemoReadListener(HEAD_SIZE)
        ).headRowNumber(3).sheet().doReadSync();
    }

    @Override
    public String createBatchId() {
        //生成唯一batchId
        return IMPORT_PREFIX + idGenerator.nextId();
    }

    @Override
    public List<ImportDataTmpDO> assembleData(List<ImportDemoDO> dataList, ImportFileUrlRequest request) {
        List<ImportDataTmpDO> temps = new ArrayList<>();
        dataList.forEach(data -> {
            ... 可以根据不同的业务逻辑,填充实体
            //组装ImportDataTmpDO
            ImportDataTmpDO tmp = assembleTmp(data, request.getStaffId(), batchId.get(), data.getLineNum());
            //调用规则引擎校验excel数据,规则引擎的实现可以翻看之前的文章
            checkData(data, tmp);
            temps.add(tmp);
        });
        return temps;
    }

    @Override
    public boolean doImportSuccessData(String batchId) {
        //校验是否有失败的数据
        int failedCounts = this.tempGateway.count(batchId, Collections.singletonList(ImportDataConstants.IMPORT_RESULT_FAILED));
        if (failedCounts > 0) {
            throw new ContractBaseException(ResponseCodeEnum.DATA_ERROR);
        }

        List<ImportDataTmpDO> list = this.tempGateway.listImportDataTmp(batchId);
        List<DemoDO> dataList = convert2DemoDataList(list);
        //存储业务数据
        this.demoGateway.saveBatch(dataList);
        return false;
    }

    private void checkData(ImportDemoDO data, ImportDataTmpDO tmp) {
        RuleContext<ImportDemoDO> context = new RuleContext<>();
        context.setRuleTypes(Arrays.asList(RuleTypeEnum.DEMO));
        context.setData(data);
        checkData(context, tmp);
    }
}

3.2 工厂模式

前文中,细心的童鞋应该已经发现在抽象父类中有个init()方法,会初始化所有的子类注册到工厂ProcessorHolder中。

3.2.1 标识子类的枚举ImportTypeEnum
@Getter
public enum ImportTypeEnum {
    ... 省略其他的业务处理枚举

    DEMO(40,"DEMO测试"),

    ;

    private final int value;
    private final String message;

    ImportTypeEnum(int value, String message) {
        this.value = value;
        this.message = message;
    }

    /**
     * 整形值转换为枚举类
     *
     * @param value 值
     * @return 枚举类
     */
    public static ImportTypeEnum valueOf(int value) {
        for (ImportTypeEnum anEnum : values()) {
            if (value == anEnum.getValue()) {
                return anEnum;
            }
        }
        return null;
    }
}
3.2.2 工厂类ProcessorHolder
public final class ProcessorHolder {

    private static Map<ImportTypeEnum, ImportProcessor> processors = new HashMap<>(64);

    public static void putProcessor(ImportTypeEnum bizType, ImportProcessor handler) {
        processors.put(bizType, handler);
    }

    public static ImportProcessor getProcessor(ImportTypeEnum bizType) {
        ImportProcessor importProcessor = processors.get(bizType);
        if (importProcessor == null) {
            throw new ContractBaseException(ResponseCodeEnum.IMPORT_TYPE_NOT_EXISTS);
        }
        return importProcessor;
    }
}
3.2.3 工厂类的调用

service层中,基于不同的业务场景,使用ImportTypeEnum从工厂中获取对应的实现。
例如:

    public ImportResultDTO importDemoFile(ImportFileUrlRequest request) {
        return ProcessorHolder.getProcessor(ImportTypeEnum.DEMO).process(request.getUrl(), request);
    }

4. 特别关注的点

4.1 EasyExcel的使用

相对于自己封装解析Excel的方法来说,EasyExcel挺好用的,方便且功能全面,是一个比较成熟的框架。

4.1.1. ReadListener

对于不同的Excel数据,可以实现ReadListener可以在解析开始解析过程解析后自定义解析逻辑。

在这里插入图片描述

当然,最简单的读取Excel场景,也无需上述ReadListener,只需要一行代码就可以搞定。

EasyExcel.read(this.getInputStream(request.getUrl())).headRowNumber(3).sheet().doReadSync();
4.1.2 Excel实体字段类型转换问题

很多时候,用户在使用Excel文件时会给你一个惊喜,比如在应该填数值类型的字段填上了奇怪的字符,这种情况下Excel会在第一步读取的时直接就失败了。
对于上述场景我们可以实现Convert,来处理字段类型转换异常问题。
如下BigDecimalConverter

@Slf4j
public class BigDecimalConverter implements Converter<BigDecimal> {

    @Override
    public BigDecimal convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        if (cellData.getNumberValue() != null) {
            return cellData.getNumberValue();
        }

        if (StrUtil.isNotBlank(cellData.getStringValue())) {
            try {
                return NumberUtils.parseBigDecimal(cellData.getStringValue(), contentProperty);
            } catch (Exception e) {
                log.info("excel列转换异常:{}", e.toString());
                return null;
            }
        }
        return null;
    }
}

4.2 规则引擎的使用

再次给大家安利下我们项目中轻量级规则引擎的实现,可查看下文。
基于Aviator开发一个简单的规则引擎

对于不同的业务导入场景,可在规则引擎中配置json文件,结合自定义function,对Excel中的数据进行业务层面的校验。

在这里插入图片描述


总结

经过通用化改造后,我们封装了通用的Excel导入流程,并且对于可变部分进行了抽象,对新增的业务导入流程开发,开发中只需关注业务逻辑,而无需处理导入流程。

新增流程时只需关注如下几点:

  1. 定义一个新的ImportTypeEnum枚举。
  2. 继承抽象父类AbstractImportProcessor,例如上述中的ImportDemoProcessor,实现业务逻辑。
  3. 按需实现ReadListener
  4. 按需调用规则引擎。
  5. service层使用只需要一行代码ProcessorHolder.getProcessor(ImportTypeEnum.DEMO).process(request.getUrl(), request);

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

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

相关文章

纹理贴图必须要输入顶点坐标或纹理坐标吗

最近知识星球的一位同学,面试时被问到:纹理贴图必须要输入顶点坐标或纹理坐标吗? 他一下子被这个问题问蒙了,虽然他知道正确答案是否定的,但是说不上来理由。 这个就引出了文本提到的全屏三角形,它不需要顶点缓冲区,而是利用顶点着色器直接生成所需的顶点坐标和纹理坐标…

【CTS】android CTS测试

android CTS测试 1.硬件准备2. 软件准备3. 下载 CTS3.1 cts3.2 解压 CTS 包&#xff1a; 4 配置adb fastboot5 检查 Java 版本6 安装aapt26.1 下载并安装 Android SDK6.2 找到 aapt2 工具6.3 配置环境变量 7. 准备测试设备8. 运行 CTS 测试8.1 启动 CTS&#xff1a; 9. 查看测试…

DDD架构和微服务初步实现

本次记录的是微服务的初步认识和DDD架构的初步实现和思路&#xff0c;在之前的发布里&#xff0c;对Javaweb进行了一次小总结&#xff0c;还有一些东西&#xff0c;不去详细理解说明了&#xff0c;下面开始我对微服务的理解。 什么是微服务&#xff1f; 在刚刚开始学习的时候…

【让AI写高考AI话题作文】看各大模型的回答

文章目录 命题chatGPT问题的消失&#xff0c;思考的萎缩 通义千问标题&#xff1a;在信息洪流中寻找智慧之光 文心一言探寻未知&#xff0c;拥抱无限的问题 命题 阅读下面的材料&#xff0c;根据要求写作。&#xff08;60分&#xff09; 随着互联网的普及、人工智能的应用&am…

快速锁定Bug!掌握Wireshark等抓包技术,提升测试效率

前言 相信做了测试一段时间的小伙伴都会开始意识到抓包对于测试的重要性&#xff0c;它涉及到功能测试、性能测试、自动化测试、安全测试和数据库测试等等。可以说我们要想做好测试就必须和抓包打交道&#xff0c;脱离抓包的测试是不合格的。人们都说黑客利用Wireshark等抓包工…

未来校园的新质生产力:南京江北新区浦口外国语学校校园网升级改造的启示

作者:南京江北新区浦口外国语学校 校长助理 杨美玲 导语:在南京江北新区(第十三个国家级新区),浦口外国语学校,这所拥有77605平方米宽阔校园、169个班级、7335名学生和511位专任教师的九年一贯制公办外语特色学校,正以前所未有的活力和智慧,迎接信息化时代的挑战。作为学校信息…

【JMeter接口测试工具】第二节.JMeter基本功能介绍(下)【进阶篇】

文章目录 前言八、Jmeter常用逻辑控制器 8.1 如果&#xff08;if&#xff09;控制器 8.2 循环控制器 8.3 ForEach控制器九、Jmeter关联 9.1 正则表达式提取器 9.2 xpath提取器 9.3 JSON提取器十、跨越线程组传值 10.1 高并发 10.2 高频…

1996-2023年各省农林牧渔总产值数据(无缺失)

1996-2023年各省农林牧渔总产值数据&#xff08;无缺失&#xff09; 1、 时间&#xff1a;1996-2023年 2、 来源&#xff1a;国家统计局、统计年鉴 3、 指标&#xff1a;农林牧渔总产值 4、 范围&#xff1a;31省 5、 缺失情况&#xff1a;无缺失 6、 指标解释&…

韩顺平0基础学java——第20天

p407-429 接口 一个类可以实现多个接口&#xff08;电脑上可以有很多插口&#xff09; class computer IB&#xff0c;IC{} 接口中的属性只能是final&#xff0c;并且是public static final 接口不能继承其他类&#xff0c;但是可以继承多个别的接口 interface ID extends I…

【PX4-AutoPilot教程-TIPS】离线安装Flight Review PX4日志分析工具

离线安装Flight Review PX4日志分析工具 安装方法 安装方法 使用Flight Review在线分析日志&#xff0c;有时会因为网络原因无法使用。 使用离线安装的方式使用Flight Review&#xff0c;可以在无需网络的情况下使用Flight Review网页。 安装环境依赖。 sudo apt-get insta…

Rust基础学习-标准库

栈和堆是我们Rust代码在运行时可以使用的内存部分。Rust是一种内存安全的编程语言。为了确保Rust是内存安全的&#xff0c;它引入了所有权、引用和借用等概念。要理解这些概念&#xff0c;我们必须首先了解如何在栈和堆中分配和释放内存。 栈 栈可以被看作一堆书。当我们添加更…

数据库错误[ERR] 1071 - Specified key was too long; max key length is 1000 bytes

环境&#xff1a;phpstudy的mysql8 索引长度问题&#xff1a; 试了很多解决办法&#xff0c;例如需改配置&#xff1a; set global innodb_large_prefixON; set global innodb_file_formatBARRACUDA; 试了还是有问题&#xff0c;直接启动不了了。因为mysql8取消了这个配置。…

Linux操作系统学习:day02

内容来自&#xff1a;Linux介绍 视频推荐&#xff1a;[Linux基础入门教程-linux命令-vim-gcc/g -动态库/静态库 -makefile-gdb调试]( day02 5、Linux目录结构 操作系统文件结构的开始&#xff0c;只有一个单独的顶级目录结构&#xff0c;叫做根目录。所有一切都从“根”开始…

DHCP原理与配置(Linux)

目录 DHCP概念 使用DHCP的好处 DHCP的分配方式 DHCP租约过程 租约过程分4个步骤&#xff08;全过程广播&#xff09; 1. 客户机请求IP&#xff08;Discover&#xff1a;发现&#xff1b;客户端广播 发送一个数据包&#xff0c;其他主机也能接收到&#xff0c;如果是没有安…

34.打印K型

上海市计算机学会竞赛平台 | YACSYACS 是由上海市计算机学会于2019年发起的活动,旨在激发青少年对学习人工智能与算法设计的热情与兴趣,提升青少年科学素养,引导青少年投身创新发现和科研实践活动。https://www.iai.sh.cn/problem/76 题目描述 小爱想用 * 打出一个大写的 K。…

边缘计算采集网关解决方案:为企业提供高效、灵活的数据处理方案-天拓四方

一、企业背景 某大型制造企业&#xff0c;位于国内某经济发达的工业园区内&#xff0c;拥有多个生产线和智能化设备&#xff0c;致力于提高生产效率、降低运营成本。随着企业规模的扩大和生产自动化的推进&#xff0c;该企业面临着海量数据处理、实时响应和网络安全等多重挑战…

P4. 微服务: 匹配系统(上)

P4. 微服务: 匹配系统 上 Tips0 概述1 匹配系统流程2 游戏系统流程3 websocket 前后端通信的基础配置3.1 websocket 的需要的配置3.2 websocket 连接的建立3.3 为 websocket 连接添加 jwt 验证 4 实现匹配界面和对战界面的切换5 匹配系统的客户端和 websocket 后端交互部分5.1 …

启明智显工业级HMI芯片Model3功耗特性分享

前言&#xff1a; 【启明智显】专注于HMI&#xff08;人机交互&#xff09;及AIoT&#xff08;人工智能物联网&#xff09;产品和解决方案的提供商&#xff0c;我们深知彩屏显示方案在现代物联网应用中的重要性。为此&#xff0c;我们一直致力于为客户提供彩屏显示方案相关的技…

MySQL系列-语法说明以及基本操作(一)

1、前言 主要讲解MySQL的基本语法 官网文档 https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html 关于MySQL的基本语法&#xff0c;关于数据类型、表的操作、数据操作、事务、备份等&#xff0c;可参考 http://www.voidme.com/mysql 2、数据类型 数…

ARM32开发--PWM高级定时器

目录 文章目录 前言 目标 学习内容 需求 高级定时器通道互补输出 开发流程 通道配置 打开互补保护电路 完整代码 练习题 总结 前言 在嵌入式软件开发中&#xff0c;PWM&#xff08;脉冲宽度调制&#xff09;技术被广泛应用于控制各种电子设备的亮度、速度等参数。…