EasyExcel - 行合并策略(二级列表)

😼前言:博主在工作中又遇到了新的excel导出挑战:需要导出多条文章及其下联合作者的信息,简单的来说是一个二级列表的数据结构。
🕵️‍♂️思路:excel导出实际上是一行一行的记录,再根据条件对其进行合并。

目录

  • 最终效果图📌
  • 一、数据格式及处理📚
    • 1.文章对象(处理前)
    • 2.文章及作者对象(处理后)
    • 3.未合并的效果图
  • 二、通用行合并策略🔍
    • 1.源码学习
    • 2.通用行合并后的效果图
  • 三、二级通用行合并策略✍
    • 1.源码改造
    • 2.设置excel输出策略
    • 3.延伸
  • 四、问题☔
    • 1.问题描述
    • 2.问题原因
    • 3.解决办法
  • 参考文章📒

最终效果图📌

最终导出格式

一、数据格式及处理📚

首先,需要先将一条文章按联合作者数量,拆分为指定数量的文章及作者导出记录的集合。

以文章《牧区歌与马》为例,一篇文章有三名联合作者,生成三条导出记录。

1.文章对象(处理前)

// 文章记录对象 Acticle.class
[{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","authorList": [{"userName": "小A ","orgName": "单位A"},{"userName": "小B ","orgName": "/"},{"userName": "小C","orgName": "单位C"}]
}]

2.文章及作者对象(处理后)

可以看到记录由一条变为三条,除了作者名称和单位,其余字段内容均一致。

// 文章处理后记录对象 ActicleAuthor.class
[{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小A""authorUnit":"单位A"
},{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小B""authorUnit":"/"
},{"contentId": "1","contentTitle": "牧区歌与马","contentCount": 940,"releaseTime": "2025-01-09 11:21:16","readNum": 1,"auditor": "小李","orgName": "办公室","author":"小C""authorUnit":"单位C"
}]

3.未合并的效果图

未设置行合并策略直接导出时,表格的格式内容如下:
未合并前的导出格式
👆图中的E、F列示例错误,应分别为6行记录

二、通用行合并策略🔍

此处学习了csdn博主xiao谢同学分享的通用行合并策略源码
链接:EasyExcel 通用行合并策略实现

1.源码学习

🐱‍👓该策略以列的行数作为主键,每次遍历记录列的最新合并区域信息。将同列且相邻行的单元格内容进行比较:
(1)一致:则仅更新endRow和endCell,继续遍历;
(2)不一致:则将已有区域进行合并,再将MergeRange所有字段进行更新。

MergeRange.class

  • startRow :合并开始行
  • endRow:合并结束行
  • startCell:合并开始单元格
  • endCell:合并结束单元格
  • lastValue:列最新单元格内容
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;import java.util.*;public class MergeStrategy extends AbstractMergeStrategy {// 合并的列编号,从0开始,指定的index或自己按字段顺序数private Set<Integer> mergeCellIndex = new HashSet<>();// 数据集大小,用于区别结束行位置private Integer maxRow = 0;// 禁止无参声明private MergeStrategy() {}public MergeStrategy(Integer maxRow, int... mergeCellIndex) {Arrays.stream(mergeCellIndex).forEach(item -> {this.mergeCellIndex.add(item);});this.maxRow = maxRow;}// 记录上一次合并的信息private Map<Integer, MergeRange> lastRow = new HashedMap();// 每行每列都会进入,绝对不要在这写循环@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {int currentCellIndex = cell.getColumnIndex();// 判断该列是否需要合并if (mergeCellIndex.contains(currentCellIndex)) {String currentCellValue = cell.getStringCellValue();int currentRowIndex = cell.getRowIndex();if (!lastRow.containsKey(currentCellIndex)) {// 记录首行起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));return;}//有上行这列的值了,拿来对比.MergeRange mergeRange = lastRow.get(currentCellIndex);if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {// 结束的位置触发下合并.// 同行同列不能合并,会抛异常if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));}// 更新当前列起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));}// 合并行 + 1mergeRange.endRow += 1;// 结束的位置触发下最后一次没完成的合并if (relativeRowIndex.equals(maxRow - 1)) {MergeRange lastMergeRange = lastRow.get(currentCellIndex);// 同行同列不能合并,会抛异常if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));}}}}
}class MergeRange {public int startRow;public int endRow;public int startCell;public int endCell;public String lastValue;public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {this.startRow = startRow;this.endRow = endRow;this.startCell = startCell;this.endCell = endCell;this.lastValue = lastValue;}
}

2.通用行合并后的效果图

可以发现,这种仅根据相邻行单元格内容进行合并的方式,还未能完全满足博主想要二级列表的效果。
简单行合并的导出格式
👆黄色代表非理想合并的区域

三、二级通用行合并策略✍

🐱‍💻改造思路:列A是文章标题,以列A的内容作为第一层级的标识(tip:不如contentId准确)。即使相邻行单元格内容相等,对应行的A列内容不相等也不能合并。

处理步骤:
(1)在合并区域对象类MergeRange中,增加A列内容的值字段lastValueRowa
(2)遍历单元格构造合并区域对象时,记录A列内容值以此来作为附加的合并条件。
(3)当同一列字段相邻行内容相等且A列内容值相等时,再进行合并。

1.源码改造

import cn.hutool.json.JSONUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;import java.util.*;/*** 行合并策略*/
@Slf4j
public class MergeStrategy extends AbstractMergeStrategy {// 合并的列编号,从0开始,指定的index或自己按字段顺序数private Set<Integer> mergeCellIndex = new HashSet<>();// 数据集大小,用于区别结束行位置private Integer maxRow = 0;// 禁止无参声明private MergeStrategy() {}public MergeStrategy(Integer maxRow, int... mergeCellIndex) {Arrays.stream(mergeCellIndex).forEach(item -> {this.mergeCellIndex.add(item);});this.maxRow = maxRow;}// 记录上一次合并的信息private Map<Integer, MergeRange> lastRow = new HashedMap();// 每行每列都会进入,绝对不要在这写循环@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {// 获取单元格当前列int currentCellIndex = cell.getColumnIndex();log.info("遍历单元格:{}行,{}列 >>>>>>>>>>>>>>>>>", cell.getRowIndex(), currentCellIndex);// 判断该列是否需要合并if (mergeCellIndex.contains(currentCellIndex)) {// 获取当前单元格内容值String currentCellValue = new DataFormatter().formatCellValue(cell);// 获取当前单元格的行rowint currentRowIndex = cell.getRowIndex();// 获取当前行A列单元格内容 newString currentValueRowa = "";// 如果最后合并行的map,不包括当前列if (!lastRow.containsKey(currentCellIndex)) {log.info("lastRow添加第{}列【前】,当前lastRow={}", currentCellIndex, JSONUtil.toJsonStr(lastRow));// 获取当前行的列A内容 newif (currentCellIndex == 0) {currentValueRowa = currentCellValue;} else {currentValueRowa = getRowaValue();}// 记录首行起始位置? 记录当前列及合并范围lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));log.info("lastRow添加第{}列【后】,当前行列A内容={},新lastRow={}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));return;} else {// 该列已存在lastRow中,则取最新的列A内容值 newcurrentValueRowa = getRowaValue();}//有上行这列的值了,拿来对比.MergeRange mergeRange = lastRow.get(currentCellIndex);// 判断条件:增加A列内容判断 newlog.info("合并比对1>>>>>>>>>:第{}列最新内容lastValue = {},当前内容currentCellValue={},", currentCellIndex, mergeRange.lastValue, currentCellValue);log.info("合并比对2>>>>>>>>>第{}列最新列A内容lastValueRowa={}, 当前列A内容currentValueRowa={}", currentCellIndex, mergeRange.lastValueRowa, currentValueRowa);if (!(mergeRange.lastValue != null&& mergeRange.lastValue.equals(currentCellValue) && mergeRange.lastValueRowa.equals(currentValueRowa))) {// 结束的位置触发下合并.// 同行同列不能合并,会抛异常if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));}// 更新当前列起始位置lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));log.info("比对不一致,确认合并!!>>>>>>>>>:第{}列最新列A内容={},最新lastRow = {}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));}// 合并行 + 1mergeRange.endRow += 1;// 结束的位置触发下最后一次没完成的合并if (relativeRowIndex.equals(maxRow - 1)) {MergeRange lastMergeRange = lastRow.get(currentCellIndex);// 同行同列不能合并,会抛异常if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));}}}}/*** 获取列A最新一行的内容值 new*/private String getRowaValue() {// 获取当前行A列单元格内容String currentValueRowa = "";if (lastRow.get(0) != null) {currentValueRowa = lastRow.get(0).lastValue;}return currentValueRowa;}
}class MergeRange {public int startRow;public int endRow;public int startCell;public int endCell;public String lastValue;// 最后一个合并值得A列值 newpublic String lastValueRowa;public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell, String lastValueRowa) {this.startRow = startRow;this.endRow = endRow;this.startCell = startCell;this.endCell = endCell;this.lastValue = lastValue;this.lastValueRowa = lastValueRowa;}
}

2.设置excel输出策略

  // 设置excel输出策略EasyExcel.write(fileName, ActicleAuthor.class)// 0,1 表示 对1,2列启用合并策略.registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) .sheet(0).doWrite(dataList);

3.延伸

🐱‍🚀如果需要做三级、四级等列表,可以将指定多个字段的拼接值当作列A来处理。可修改getRowaValue()方法实现逻辑。

四、问题☔

在开发的过程中,不可避免地碰到了一些问题……

1.问题描述

💁‍♀️在获取列A内容值时,曾尝试从Sheet对象中获取。
因为存在Sheet.getRow(0)获取第一行的row对象是null的问题,所以用从lastRow中获取列A内容的方法进行替代。

/*** 从sheet.getRow(0)中获取列A内容值*/private String getRowaValue(Sheet sheet, int rowId) {// 获取当前行A列单元格内容String currentValueRowa = "";Row row = sheet.getRow(rowId);if (row != null) {Cell cell = row.getCell(0);if (cell != null) {currentValueRowa = cell.getStringCellValue();}}return currentValueRowa;}

2.问题原因

👩‍💻经过面向百度查询,从csdn博主吾乃南华老仙分享的文章sheet.getRow(0)获取的row为null?中得知:

new SXSSFWorkbook(new XSSFWorkbook(inputStream)) 创建Workbook的时候,
SXSSFWorkbook对象内部会维护一个HashMap(反编译后的名称为_xFromSxHash)。

而当使用workBook.getSheetAt(0)的时候,其实是从_xFromSxHash中获取新创建的Sheet对象,从而导致sheet.getRow(0)获取的row为null。

😸文中提供的解决方法:

将获取首行代码:
Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet = workBook.getSheetAt(0);
Row row = sheet.getRow(0);修改为👇:Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet;
if (workBook instanceof SXSSFWorkbook) {SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) workBook;sheet = sxssfWorkbook.getXSSFWorkbook().getSheetAt(sheetIndex);
} else {sheet = workBook.getSheetAt(sheetIndex);
}
Row row = sheet.getRow(0);

😧而我们使用的导出是基于EasyExcel的,并没有单独的使用流去创建对象,应该怎么办呢?

👇可以看到debug过程中,显示sheet对象类型是SXSSFSheet。
在这里插入图片描述

3.解决办法

😾再次经过一番查询,在EasyExcel语雀文档的QA:EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释中找到了解释:

  • inMemory(true):
    EasyExcel默认使用SXSSFWorkbook以减少内存消耗,但它不支持复杂的样式设置(如水印)。通过设置inMemory(true),我们改用XSSFWorkbook,它提供了更全面的样式支持。

在设置excel输出策略时加上inMemory(true)设置:

  // 设置excel输出策略EasyExcel.write(fileName, ActicleAuthor.class)// 必须设置,以便使用XSSFWorkbook而非SXSSFWorkbook new.inMemory(true)// 0,1 表示 对1,2列启用合并策略.registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) .sheet(0).doWrite(dataList);

🙆‍♀️执行debug后,发现可以获取到sheet.getRow(0)的第一行对象了,切换类型的问题成功解决。
在这里插入图片描述

参考文章📒

EasyExcel 通用行合并策略实现-xiao谢同学
sheet.getRow(0)获取的row为null?
EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释

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

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

相关文章

第9章:基于Vision Transformer(ViT)网络实现的迁移学习图像分类任务:早期秧苗图像识别

目录 1. ViT 模型 2. 早期秧苗分类 2.1 数据集 2.2 训练 2.3 训练结果 2.4 可视化网页推理 3. 下载 1. ViT 模型 视觉变换器&#xff08;ViT&#xff09;是一种神经网络架构&#xff0c;它将变换器架构的原理应用于视觉数据。最初&#xff0c;Transformers主要用于自然…

ros2-7.5 做一个自动巡检机器人

7.5.1 需求及设计 又到了小鱼老师带着做最佳实践项目了。需求&#xff1a;做一个在各个房间不断巡逻并记录图像的机器人。 到达目标点后首先通过语音播放到达目标点信息&#xff0c; 再通过摄像头拍摄一张图片保存到本地。 7.5.2 编写巡检控制节点 在chapt7_ws/src下新建功…

OpenHarmony API 设计规范

OpenHarmony API 设计规范 修订记录 版本作者时间更新内容v0.1&#xff0c;试运行版OpenHarmony API SIG2022年11月初版发布 目的 API是软件实现者提供给使用者在编程界面上的定义&#xff0c;API在很大程度上体现了软件实体的能力范围。 同时&#xff0c;API定义的好坏极…

【React】新建React项目

目录 create-react-app基础运用React核心依赖React 核心思想&#xff1a;数据驱动React 采用 MVC体系package.jsonindex.html好书推荐 官方提供了快速构建React 项目的脚手架&#xff1a; create-react-app &#xff0c;目前使用它安装默认是19版本&#xff0c;我们这里降为18…

Linux手写FrameBuffer任意引脚驱动spi屏幕

一、硬件设备 开发板&#xff1a;香橙派 5Plus&#xff0c;cpu&#xff1a;RK3588&#xff0c;带有 40pin 外接引脚。 屏幕&#xff1a;SPI 协议 0.96 寸 OLED。 二、需求 主要是想给板子增加一个可视化的监视器&#xff0c;并且主页面可调。 平时跑个模型或者服务&#xff0c;…

网络安全构成要素

一、防火墙 组织机构内部的网络与互联网相连时&#xff0c;为了避免域内受到非法访问的威胁&#xff0c;往往会设置防火墙。 使用NAT&#xff08;NAPT&#xff09;的情况下&#xff0c;由于限定了可以从外部访问的地址&#xff0c;因此也能起到防火墙的作用。 二、IDS入侵检…

React Native的现状与未来:从发展到展望

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

数据结构——链表(概念,类型,java实现、增删、优缺点)

我是一个计算机专业研0的学生卡蒙Camel&#x1f42b;&#x1f42b;&#x1f42b;&#xff08;刚保研&#xff09; 记录每天学习过程&#xff08;主要学习Java、python、人工智能&#xff09;&#xff0c;总结知识点&#xff08;内容来自&#xff1a;自我总结网上借鉴&#xff0…

app版本控制java后端接口版本管理

java api version 版本控制 java接口版本管理 1 自定义 AppVersionHandleMapping 自定义AppVersionHandleMapping实现RequestMappingHandlerMapping里面的方法 public class AppVersionHandleMapping extends RequestMappingHandlerMapping {Overrideprotected RequestCondit…

LRU 算法详解与 Java 的两种实现方式

LRU 算法详解与 Java 的两种实现方式 LRU 算法详解与 Java 的两种实现方式一、LRU 算法简介二、LRU 算法原理三、LRU 算法应用场景四、Java 实现 LRU 算法(一)基于 LinkedHashMap 实现(二)基于双向链表和 HashMap 实现五、总结LRU 算法详解与 Java 的两种实现方式 一、LRU…

基于 Python 的财经数据接口库:AKShare

AKShare 是基于 Python 的财经数据接口库&#xff0c;目的是实现对股票、期货、期权、基金、外汇、债券、指数、加密货币等金融产品的基本面数据、实时和历史行情数据、衍生数据从数据采集、数据清洗到数据落地的一套工具&#xff0c;主要用于学术研究目的。 安装 安装手册见…

在 macOS 上,用命令行连接 MySQL(/usr/local/mysql/bin/mysql -u root -p)

根据你提供的文件内容&#xff0c;MySQL 的安装路径是 /usr/local/mysql。要直接使用 mysql 命令&#xff0c;你需要找到 mysql 可执行文件的路径。 在 macOS 上&#xff0c;mysql 客户端通常位于 MySQL 安装目录的 bin 子目录中。因此&#xff0c;完整的路径应该是&#xff1…

【QT】: 初识 QWidget 控件 | QWidget 核心属性(API) | qrc 文件

&#x1f525; 目录 1. 控件概述 控件体系的发展阶段 2. QWidget 核心属性 2.1 核心属性概览2.2 用件可用&#xff08;Enabled&#xff09; 2.3 坐标系&#xff08;Geometry&#xff09; **实例 1: 控制按钮的位置**实例 2: 表白 程序 2.4 窗口标题&#xff08;windowTiltle&a…

通过外部链接启动 Flutter App(详细介绍及示例)

通过外部链接启动 Flutter App&#xff08;firebase_dynamic_links 和 app_links&#xff09; 详细介绍 通过外部链接启动flutter App 的使用及示例 在我们的APP中&#xff0c;经常有点击链接启动并进入APP的需求&#xff08;如果未安装跳转到应用商店&#xff09;。Android通…

git操作(Windows中GitHub)

使用git控制GitHub中的仓库版本&#xff0c;并在Windows桌面中创建与修改代码&#xff0c;与GitHub仓库进行同步。 创建自己的GitHub仓库 创建一个gen_code实验性仓库用来学习和验证git在Windows下的使用方法&#xff1a; gen_code仓库 注意&#xff0c;创建仓库时不要设置…

MySQL DCL 数据控制

文章目录 1.新建用户2.删除用户3.用户授权4.撤销用户权限5.查看用户权限6.修改用户密码7.权限类型参考文献 1.新建用户 连接到 MySQL 服务器后&#xff0c;管理员或特权用户可以使用 CREATE USER 语句创建新用户。 CREATE USER usernamehost IDENTIFIED BY password;-- 示例 …

python爬虫爬取淘宝商品比价||淘宝商品详情API接口

最近在学习北京理工大学的爬虫课程&#xff0c;其中一个实例是讲如何爬取淘宝商品信息&#xff0c;现整理如下&#xff1a; 功能描述&#xff1a;获取淘宝搜索页面的信息&#xff0c;提取其中的商品名称和价格 探讨&#xff1a;淘宝的搜索接口 翻页的处理 技术路线:requests…

【Vim Masterclass 笔记13】第 7 章:Vim 核心操作之——文本对象与宏操作 + S07L28:Vim 文本对象

文章目录 Section 7&#xff1a;Text Objects and MacrosS07L28 Text Objects1 文本对象的含义2 操作文本对象的基本语法3 操作光标所在的整个单词4 删除光标所在的整个句子5 操作光标所在的整个段落6 删除光标所在的中括号内的文本7 删除光标所在的小括号内的文本8 操作尖括号…

深度学习 Pytorch 张量的索引、分片、合并以及维度调整

张量作为有序的序列&#xff0c;也是具备数值索引的功能&#xff0c;并且基本索引方法和python原生的列表、numpy中的数组基本一致。 不同的是&#xff0c;pytorch中还定义了一种采用函数来进行索引的方式。 作为pytorch中的基本数据类型&#xff0c;张量既具备了列表、数组的基…

强推未发表!3D图!Transformer-LSTM+NSGAII工艺参数优化、工程设计优化!

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Transformer-LSTMNSGAII多目标优化算法&#xff0c;工艺参数优化、工程设计优化&#xff01;&#xff08;Matlab完整源码和数据&#xff09; Transformer-LSTM模型的架构&#xff1a;输入层&#xff1a;多个变量作…