
导入导出是平台基于Apache#POI的excel读写Api,并结合苍穹的动态领域模型提供的一套ETL(数据仓库)标准功能。
导入、导出均以操作的形式呈现,通过在设计器中给单据配置相应的操作,可给对应单据添加导入导出功能。
## 1:导入
### 1.1 导入概述
导入的实质是**模拟**单据保存。
导入的实现与苍穹的插件体系息息相关,通过展示一系列的单据页面:导入起始页,下称“起始页”;导入进度条页面,下称“进度页”,导入结果页,下称“结果页”。各页面绑定的插件在初始化、点击等方法中,完成导入初始化工作的准备、导入线程的启动、导入结果的统计、错误文件的输出。导入主体逻辑为生产(解析)消费者(导入)模式。全流程图如下:

在导入过程中 ,二次开发可通过继承BatchImportPlugin类并重写部分方法,子类需注册至“导入”操作插件中

平台提供多个插件入口供二次开发进行扩展,以调整适配自身业务。下面逐步讲解:
### 1.2 导入起始页
点击列表“导入数据”操作,系统将展示起始页
起始页在其初始化方法中,调用kd.bos.form.plugin.impt.BatchImportPlugin的方法,设定起始页的控件样式,默认值等。
可影响起始页的方法如下:
| 插件方法名 | 作用 | 二开场景 |
| ----------------------- | ------------------------------------------ | ------------------------------------------------------------ |
| getBillFormId | 获取待导入的单据标识 | A单与B单共用同一张数据表,在A单中操作导入,需导入保存的数据为B单数据时 |
| getOverrideFieldsConfig | 获取更新导入匹配字段的下拉项集合 | 打开起始页时,自定义更新导入的匹配字段下拉框的下拉选项值 |
| getDefaultKeyFields | 获取更新导入匹配字段的默认值 | 打开起始页时,自定义更新导入的匹配字段下拉框的默认值 |
| getDefaultImportType | 获取导入方式默认值,新增、更新、更新并新增 | 打开起始页时,自定义默认的导入方式(如设置为更新) |
| getDefaultLockUIs | 获取默认锁定的控件列表 | 打开起始页时,禁用更新导入下拉框 |
上述方法作用的时序图如下(时序图1):起始页初始化方法中调取“resolve()”

起始页初始化完成,用户上传待导入的Excel文件后,点击开始导入,将进入进度框页面。进度框页面在其初始化方法中,启动“解析”线程(线程方法resolveExcel)及“导入”线程(线程方法importData),“解析”线程作为生产者,将Excel的数据转换为JSON数据包,入队数据队列。“导入”线程不断从队列中尝试出队,出队后进行导入。
### 1.3 解析线程
解析线程借助Apache的POI包,将Excel文件中的数据,逐行解析,智能匹配。最终以单据为单位入队。
可影响解析线程的方法如下:
| 插件方法名 | 作用 | 二开场景 |
| ------------------- | -------------------- | ------------------------------------------------------------ |
| resolveExcel | 解析线程主方法 | 完全自定义解析过程,需同步重写importData |
| getBillFormId | 获取待导入的单据标识 | A单与B单共用同一张数据表,在A单中操作导入,需导入保存的数据为B单数据时 |
| buildMainEntityType | 构建实体类型 | 二次开发存在自定义属性,元数据中获取不到时,通过该方法构建 |
上述方法作用的时序图如下(时序图2):

### 1.4 导入线程
如全流程图所示,导入线程取完队列中数据后,对数据进行分批、转换、保存。
影响导入线程的方法如下:
| 插件方法名 | 作用 | 二开场景 |
| ---------- | -------------- | ---------------------------- |
| importData | 导入线程主方法 | 完全自定义导入的转换保存过程 |
重写该方法,则全流程图中,分批,转换,保存的方法均不再进入。
下面介绍分批、转换、保存环节,二次开发可进行干预的方法入口:
#### 1.4.1 分批
出队的数据,导入线程会将其分批进行处理,降低引入对内存的消耗。
可影响分批的方法如下:
| 插件方法名 | 作用 | 二开场景 |
| ------------------ | ------------------------ | ------------------------------------------------ |
| isForceBatch | 是否强制分批处理,默认否 | 需要队列中存在设定好的批次大小数据时,才进行导入 |
| getBatchImportSize | 获取每批次数据量大小 | 自定义分批大小 |
#### 1.4.2 转换
导入为了实现模拟保存(导入过程应与单据界面手动录单一致)的效果,需要调取平台的保存接口。而保存接口需要的入参对象为苍穹的动态对象DynamicObject。在分批之后,导入线程持有的依旧是JSON格式的数据,需要将其转换为动态对象,所以存在转换过程。
过程中生成一个无界面的单据对象进行数据转换,因此,转换过程将触发平台的表单插件,这里解释了导入操作是列表操作,却可以通过表单插件进行干预的原因。
可影响转换的方法如下(触发时机先后顺序排列):
| 表单插件方法名 | 触发时机 | 二开场景 |
| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| afterCreateNewData | 界面初始化或刷新,新建数据包完毕后。每批次开始创建数据包时触发 | 向数据包中添加默认值 |
| initImportData | 表单视图模型初始化,创建插件后,触发此事件。引入每批次引入前触发 | 批量读取基础资料 |
| beforeImportData | 本单数据开始转换前触发 | 修改数据包或取消该单导入 |
| queryImportBasedata | 查询基础资料时触发 | 当基础资料查不到或者查到多个结果时,设置正确的id。保证引入继续进行 |
| afterImportData | 数据转换完成时触发 | 保存前需修改动态对象数据,做合法性校验等 |
上述方法需继承**表单插件基类**kd.bos.entity.plugin.AbstractDataModelPlugin后才可重写。表单插件注册方式如下图:

#### 1.4.3 保存
可影响保存的方法如下:
| 插件方法名 | 作用 | 二开场景 |
| ------------------- | ---------------- | ------------------------------------------------------------ |
| save | 保存 | 自定义保存方案 |
| buildMainEntityType | 构建实体类型 | 存在代码中新增的自定义属性,元数据中获取不到时,通过该方法构建 |
| beforeSave | 保存前预处理数据 | 自定义JSON数据的校验,如必录性等 |
| getSaveWebApi | 获取保存服务实例 | 自定义保存的op类 |
**引入线程**涉及方法时序图如时序图3(“转换过程“不在此图):

### 1.5. 导入插件总览
综上,我们将导入过程中涉及到的插件事件整理如下:
其中,通过继承导入操作插件kd.bos.form.plugin.impt.BatchImportPlugin类,可重写如下方法
```java
/** 获取待导入的实体key */
public String getBillFormId()
/** 覆盖导入的匹配字段 */
public List<ComboItem> getOverrideFieldsConfig()
/** 覆盖导入匹配字段的缺省值 */
public String getDefaultKeyFields()
/** 缺省导入模式,新增、覆盖、覆盖并新增 */
public String getDefaultImportType()
/** 缺省 锁定的控件列表 */
public List<String> getDefaultLockUIs()
/** 解析excel */
protected void resolveExcel()
/** 导入数据,主要是轮询取数,并对数据分批,分析数据依赖关系,整理保存结果等 */
protected void importData()
/** 保存前预处理数据*/
protected void beforeSave(List<ImportBillData> billdatas, ImportLogger logger)
/** 保存 */
protected ApiResult save(List<ImportBillData> rowdatas, ImportLogger logger)
/** 是否强制批处理
* @return true,如果多列中不够一批将等待;false 不管队列中有多少数据,取多少用多少
*/
protected boolean isForceBatch()
/** 导入的批量数据大小 */
protected int getBatchImportSize()
/** 构建Web API保存操作实例,用于转换Json并保存单据 */
public AbstractOperateWebApi getSaveWebApi()
/** 根据数据动态构造实体类型 */
protected MainEntityType buildMainEntityType(JSONObject billdata)
/** 根据模板数据动态构造引出实体模型(导出或下载模板用) */
public MainEntityType getExportMainEntityType(String billFormId, DynamicObject templateData)
```
通过继承表单插件kd.bos.bill.AbstractBillPlugIn类,可以重写下列方法:
```java
/** 导入前初始化 */
public void initImportData(InitImportDataEventArgs e)
/** 原始数据填充model前事件 */
public void beforeImportData(BeforeImportDataEventArgs e)
/** 原始数据填充model后,保存前事件,我们可以从model获取/修改当前已经转换好的数据,也可以添加/移除/修正基础资料缓存,提高下一条数据导入的查询和命中效率 */
public void afterImportData(ImportDataEventArgs e)