|
| 1 | +# REQ-01 文件数据集导入功能 — params 重建补充设计(开源) |
| 2 | + |
| 3 | +> 配套文档:`REQ-01_文件数据集导入功能_设计_开源.md`、`REQ-01_文件数据集导入功能_fieldMap字段映射补充设计_开源.md` |
| 4 | +> |
| 5 | +> 本文档针对 **文件 source 作业编辑态加载时 `params.sources` 丢失** 问题,定位后端根因并给出修复方案,供开源团队在 `../exchangis` 实施。 |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## 1. 问题现象 |
| 10 | + |
| 11 | +保存文件 source 作业后,再次编辑加载,前端从 `GET /dss/exchangis/main/job/{id}` 拿到的 `result.jobContent` 里 `subJobs[i].params.sources` 为**空数组**: |
| 12 | + |
| 13 | +- 数据库 `exchangis_job_entity.job_content` 字段**含完整 params.sources**(前端保存时发的 saveContent,`config_key` 格式,含 `__file_bml_resource_id / __file_bml_version / __file_bml_owner / __file_bml_name` + `encoding / delimiter / nullFormat`)。 |
| 14 | +- 但接口返回的 jobContent 里 `params.sources` 为空 → 前端 dyncRender 无参数可渲染 → 文件 source 的编码/分隔符/空值格式 + BML 引用全部丢失,无法回显,用户改过的值也丢。 |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 2. 根因定位 |
| 19 | + |
| 20 | +### 2.1 调用链 |
| 21 | + |
| 22 | +``` |
| 23 | +GET /dss/exchangis/main/job/{id} |
| 24 | + ExchangisJobRestfulApi.getJob() (ExchangisJobRestfulApi.java:320) |
| 25 | + → jobInfoService.getDecoratedJob(request, id) ← "Decorated" = 装饰过,非原始 jobContent |
| 26 | + DefaultJobInfoService.getDecoratedJob() (DefaultJobInfoService.java:222) |
| 27 | + → uiGetter.getJobDataSourceUIs(request, id) ← 用 UI 配置重建 |
| 28 | + DefaultDataSourceService.getJobDataSourceUIs() (DefaultDataSourceService.java:145) |
| 29 | + → buildAllUI(request, job, cnt) (AbstractDataSourceService.java:116) |
| 30 | + → buildDataSourceParamsUI(dsIdsUI, content) |
| 31 | + → buildDataSourceParamsFilledValueUI(paramConfigs, paramsList) |
| 32 | + (AbstractDataSourceService.java:168) |
| 33 | + → jobVo.setJobContent(重建后的 content) (DefaultJobInfoService.java:234) ← 覆盖原始 |
| 34 | +``` |
| 35 | + |
| 36 | +### 2.2 致命点:`buildDataSourceParamsFilledValueUI` 完全依赖 `paramConfigs` |
| 37 | + |
| 38 | +`AbstractDataSourceService.buildDataSourceParamsFilledValueUI`(`AbstractDataSourceService.java:168-187`): |
| 39 | + |
| 40 | +```java |
| 41 | +protected List<ElementUI<?>> buildDataSourceParamsFilledValueUI( |
| 42 | + List<ExchangisJobParamConfig> paramConfigs, |
| 43 | + List<ExchangisJobParamsContent.ExchangisJobParamsItem> paramsList) { |
| 44 | + List<ElementUI<?>> uis = new ArrayList<>(); |
| 45 | + if (!Objects.isNull(paramConfigs) && !paramConfigs.isEmpty()) { // ← paramConfigs 来自 ExchangisJobParamConfig 表 |
| 46 | + for (ExchangisJobParamConfig cfg : paramConfigs) { |
| 47 | + if (Objects.isNull(paramsList) || paramsList.isEmpty()) { |
| 48 | + uis.add(fillElementUIValue(cfg, "")); |
| 49 | + continue; |
| 50 | + } |
| 51 | + ExchangisJobParamsContent.ExchangisJobParamsItem selectedParamItem = |
| 52 | + getJobParamsItem(cfg.getConfigKey(), paramsList); |
| 53 | + // ... 按 cfg 填值 ... |
| 54 | + } |
| 55 | + } |
| 56 | + return uis; // paramConfigs 空 → 返回空 list |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +`paramConfigs` 由 `ExchangisJobParamConfigMapper` 按 source type 查 `exchangis_job_param_config` 表。 |
| 61 | + |
| 62 | +**FILE 类型在该表无注册**(M5 决策:文件 source 不创建 `ExchangisDataSourceDefinition`、不注册 param config),所以 `paramConfigs` 为空 → 重建后的 `params.sources` 为空。 |
| 63 | + |
| 64 | +### 2.3 `getDecoratedJob` 用重建结果覆盖原始 jobContent |
| 65 | + |
| 66 | +`DefaultJobInfoService.getDecoratedJob`(`DefaultJobInfoService.java:222-243`): |
| 67 | + |
| 68 | +```java |
| 69 | +List<ExchangisDataSourceUIViewer> jobDataSourceUIs = this.uiGetter.getJobDataSourceUIs(request, id); |
| 70 | +ObjectMapper objectMapper = JsonUtils.jackson(); |
| 71 | +String content = objectMapper.writeValueAsString(jobDataSourceUIs); // 重建后的(params.sources 空) |
| 72 | +JsonNode contentJsonNode = objectMapper.readTree(content); |
| 73 | +ObjectNode objectNode = objectMapper.createObjectNode(); |
| 74 | +objectNode.set("subJobs", contentJsonNode); |
| 75 | +jobVo.setJobContent(objectNode.toString()); // ← 覆盖原始 jobContent |
| 76 | +``` |
| 77 | + |
| 78 | +因此接口返回的 jobContent `params.sources` 为空,而数据库原始 `job_content` 字段是完整的——"数据库有、接口没有"。 |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## 3. 为什么前端无法规避 |
| 83 | + |
| 84 | +前端 `getInfo` 只能拿到 `data.jobContent`(已被后端重建为空 params)。前端的 `restoreFileSourceParams`(`dataSource.vue`)从这个空 params 还原,自然拿不到值。即使从 `dataSourceIds.source` 拿 BML 引用调 `reparseFileSource` 重拉 parseResult,也只能拿原始解析值,**用户改过的 encoding/delimiter/nullFormat 会丢**。 |
| 85 | + |
| 86 | +**必须在后端修复**:重建时对文件 source 保留原始 params。 |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## 4. 修复方案 |
| 91 | + |
| 92 | +### 方案 A(推荐):`buildAllUI` 对文件 source 透传原始 params |
| 93 | + |
| 94 | +在 `AbstractDataSourceService.buildAllUI`(`AbstractDataSourceService.java:116-129`)里,**检测 source type == "FILE"**,不走 `ExchangisJobParamConfig` 表查询重建,**直接从原始 content 的 params.sources 透传**。 |
| 95 | + |
| 96 | +```java |
| 97 | +protected ExchangisDataSourceUIViewer buildAllUI(HttpServletRequest request, |
| 98 | + ExchangisJobEntity job, |
| 99 | + ExchangisJobInfoContent content) { |
| 100 | + ExchangisDataSourceIdsUI dataSourceIdsUI = buildDataSourceIdsUI(request, content); |
| 101 | + |
| 102 | + // ⭐ REQ-01:文件 source 无 ExchangisJobParamConfig 注册(M5),表查询重建会返回空 list, |
| 103 | + // 导致 getDecoratedJob 用空 params 覆盖原始 jobContent。此处对 FILE 直接透传原始 params.sources |
| 104 | + // (config_key 格式),前端 restoreFileSourceParams 已能处理 config_key 格式。 |
| 105 | + // File source has no param config registration (M5); table-based rebuild returns empty, which |
| 106 | + // overwrites the original jobContent. Pass through original params.sources (config_key format) |
| 107 | + // for FILE — frontend restoreFileSourceParams handles config_key format. |
| 108 | + ExchangisDataSourceParamsUI paramsUI; |
| 109 | + String sourceType = dataSourceIdsUI.getSource() == null ? null : dataSourceIdsUI.getSource().getType(); |
| 110 | + if ("FILE".equalsIgnoreCase(sourceType)) { |
| 111 | + paramsUI = buildFileSourceParamsUIFromContent(content); |
| 112 | + } else { |
| 113 | + paramsUI = buildDataSourceParamsUI(dataSourceIdsUI, content); |
| 114 | + } |
| 115 | + |
| 116 | + ExchangisJobTransformsContent transforms = content.getTransforms(); |
| 117 | + List<ElementUI<?>> jobDataSourceSettingsUI = this.buildJobSettingsUI(job.getEngineType(), content); |
| 118 | + return new DefaultDataSourceUIViewer( |
| 119 | + content.getSubJobName(), dataSourceIdsUI, paramsUI, transforms, jobDataSourceSettingsUI); |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +`buildFileSourceParamsUIFromContent(content)` 实现要点(二选一): |
| 124 | + |
| 125 | +- **简版(透传)**:把 `content.getSources()`(`List<ExchangisJobParamsContent.ExchangisJobParamsItem>`,含 `configKey / configName / configValue / sort`)转成 `List<ElementUI<?>>`(`InputElementUI`,`field=configKey, label=configName, value=configValue, sort=sort, show="_true"/"_false"`)。hidden 项(`__file_bml_*`)`show="_false"`。 |
| 126 | +- **复用版**:直接把 `content.getSources()` 包进 `ExchangisDataSourceParamsUI` 返回(前端已兼容 config_key 格式,无需转 UI)。 |
| 127 | + |
| 128 | +推荐简版(保持 viewer 输出结构一致:`params.sources` 仍是 `ElementUI` 列表,`__file_bml_*` hidden)。 |
| 129 | + |
| 130 | +### 方案 B(备选):`getDecoratedJob` 对文件 source subJob 不重建 |
| 131 | + |
| 132 | +在 `DefaultJobInfoService.getDecoratedJob`(line 222-243),检测 subJob source 是否 FILE,若是则保留原始 jobContent(不调 `getJobDataSourceUIs` 重建)。粒度较粗(整个 subJob 不装饰),可能影响 `dataSourceIds / transforms` 的 UI 装饰,**不推荐**。 |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## 5. `dataSourceIds` 的 BML 字段(需同步确认) |
| 137 | + |
| 138 | +`buildDataSourceIdsUI` → `parseDataSourceIdUi`(`AbstractDataSourceService.java:301-333`)用 `idValue.trim().split("\\.")` 拆 `type / id / db / table`: |
| 139 | + |
| 140 | +```java |
| 141 | +ExchangisDataSourceIdUI ui = new ExchangisDataSourceIdUI(jobDataSource); // 从原始 content source 读 |
| 142 | +if (StringUtils.isNotBlank(idValue)) { |
| 143 | + String[] split = idValue.trim().split("\\."); |
| 144 | + ui.setType(split[0]); |
| 145 | + ui.setId(split[1]); |
| 146 | + ui.setDb(split[2]); |
| 147 | + ui.setTable(split[3]); |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +文件 source 的 `dataSourceIds.source` 是 `{type:'FILE', id:0, resourceId, version, owner, name, path}`,没有传统 `type.ds.db.table` 字符串。需确认: |
| 152 | + |
| 153 | +1. `ExchangisDataSourceIdUI(jobDataSource)` 构造是否保留 `resourceId / version / owner / name / path`(BML 引用)—— 前端编辑态回显依赖这些。 |
| 154 | +2. `idValue` split 对 FILE type 是否会覆盖/污染 BML 字段。若会,对 `type == "FILE"` 跳过 split,保留 `jobDataSource` 原始字段。 |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## 6. 前端配合(已完成,无需开源团队改) |
| 159 | + |
| 160 | +前端 `restoreFileSourceParams`(`wedatasphere-exchangis-web/cluster/src/pages/jobManagement/components/dataSource.vue`)已实现: |
| 161 | + |
| 162 | +- 加载已保存作业时,把 `params.sources`(config_key 格式)重构为 UI 格式(`buildFileSourceParams`),值从 `config_value` 还原。 |
| 163 | +- emit `updateSourceParams` 同步 `curTask.params`,避免再次保存丢值。 |
| 164 | +- `saveAll` forEach 兜底兼容 config_key 格式项。 |
| 165 | + |
| 166 | +**前提**:接口返回的 `params.sources` 非空。本修复(方案 A)确保透传,前端即可正常回显。 |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## 7. 验证步骤 |
| 171 | + |
| 172 | +1. 前端新建文件 source 作业:上传 CSV/TXT → 改 encoding/delimiter/nullFormat → 选 sink → 字段映射 → 保存。 |
| 173 | +2. `GET /dss/exchangis/main/job/{id}` 返回的 `result.jobContent.subJobs[i].params.sources` 应含 7 项(`__file_bml_resource_id / version / owner / name` + `encoding / delimiter / nullFormat`,config_key 格式)。 |
| 174 | +3. 前端编辑态重新打开该作业:dyncRender 渲染 encoding/delimiter/nullFormat 输入框,值为用户保存值;FileUploadComponent 回显文件信息;字段映射正常。 |
| 175 | +4. 再次保存,参数不丢(前端 saveAll forEach 兜底 + curTask.params 已是 UI 格式)。 |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## 8. 涉及文件(开源仓库 `../exchangis`) |
| 180 | + |
| 181 | +| 文件 | 方法 / 行号 | 改动 | |
| 182 | +|---|---|---| |
| 183 | +| `exchangis-datasource/exchangis-datasource-service/.../AbstractDataSourceService.java` | `buildAllUI` (116) | 加 FILE 分支,透传原始 params | |
| 184 | +| 同上 | `parseDataSourceIdUi` (301) | 确认 FILE 的 BML 字段不被 split 覆盖 | |
| 185 | +| 同上 | `buildDataSourceParamsFilledValueUI` (168) | 无需改(仅作为根因定位参照) | |
| 186 | +| `exchangis-job/exchangis-job-server/.../DefaultJobInfoService.java` | `getDecoratedJob` (222) | 无需改(方案 A 在更上层拦截) | |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## 9. 备注 |
| 191 | + |
| 192 | +- 本修复不影响非文件 source(mysql/hive 等)的既有重建逻辑——只在 `source.type == "FILE"` 时走透传分支。 |
| 193 | +- 文件 source 的 `__file_bml_*` 是 hidden 参数(`show="_false"`),不渲染但进入保存 payload,后端 handler 读取。重建时需保留这些项(不能因为 hidden 就丢)。 |
0 commit comments