18|HarmonyOS材料导入列映射与错误行隔离【共创稿事节】
上一篇已经把CSV读取拆成三个层次:第三方库负责引号和换行等语法,输入边界负责文件大小与结构,HarmonyOS页面负责展示解析摘要。这时导入工作台能够稳定拿到二维字符串数组,却仍然不能直接写入材料表。原因很常见:同一工地的不同供应商对相同业务含义使用不同列名,“物料编码”可能叫“材料编号”,“规格型号”可能叫“型号”,“施工区域”可能叫“使用部位”。如果继续依据固定列号取值,清单格式稍有调整,材料数量就可能被写进单位列。
这一篇沿用工地材料导入工作台的同一条数据路径,把表头映射变成明确可审查的配置,再对每一条材料记录分别给出接受或隔离的结论。我们希望材料员一眼能回答三个问题:哪几列对应业务字段,哪几条记录有问题,以及本次确认会导入多少条有效数据。这里展示的错误行不是第三方库未能识别的破损CSV片段,而是完成语法切分后列数或业务值不满足约束的记录。

映射不能建立在列顺序恰好相同的假设上
标准业务对象先固定六个字段:materialCode、materialName、specification、unit、quantity、zone。外部清单允许“材料编号”映射到materialCode,“名称”映射到materialName,“型号”映射到specification,“计量单位”映射到unit,“入库数量”映射到quantity,“区域”映射到zone。如果供应商交换这些列的位置,数据含义仍应保持稳定。映射界面最好同时展示源字段名称、目标业务字段和样本值,不能把“猜中了一个近似词”当成用户已经确认。
映射配置需要版本号及供应商维度,避免下次导入时把另一家供应商的列别名复用。不能出现两列源字段指向同一项必填目标字段,也不能让必填业务字段缺失后仍进入提交阶段。例如某份清单同时包含“计划数量”和“入库数量”,两个字段都看起来像数量,却不能自动合并。一次性批准的映射应绑定当前表头指纹,表头改变后重新核对,而不是凭上次记忆静默放行。
我们选择先把映射算法写在可直接执行的Node.js业务层,以方便独立检查;HarmonyOS ArkUI页面把映射选择和字段说明交给操作员,最终提交通过同一数据契约。解析库的Node接口与ArkTS页面之间仍由第一篇的适配边界隔开,页面不必了解CSV的转义细节。
要解决的问题:自动映射出现同名目标冲突
简单做法是准备别名表,遍历首行,将列名标准化后匹配字段。标准化只去两侧空格、全角空格与BOM,不能移除所有标点,否则两个实际不同的字段可能被不当地合并。命中别名仍必须检查目标冲突,重复映射时给出阻断原因。源列索引保留下来,后面即便展示名称变化也能准确取到原始值。
const TARGETS = {
materialCode: ['物料编码', '材料编号', '物料编号'],
materialName: ['物料名称', '名称'],
specification: ['规格型号', '型号'],
unit: ['单位', '计量单位'],
quantity: ['数量', '入库数量'],
zone: ['施工区域', '使用部位', '区域']
};
function buildColumnMap(headers) {
const normalize = name => String(name).replace(/^\uFEFF/, '').trim();
const indexMap = new Map();
for (const [target, aliases] of Object.entries(TARGETS)) {
const matches = headers.map((name, index) => ({name: normalize(name), index}))
.filter(item => aliases.includes(item.name));
if (matches.length !== 1) throw new Error(`字段 ${target} 匹配到 ${matches.length} 列`);
indexMap.set(target, matches[0].index);
}
if (new Set([...indexMap.values()]).size !== indexMap.size) {
throw new Error('不同业务字段复用了同一列');
}
return Object.fromEntries(indexMap);
}
这个函数不依赖列顺序,而且在冲突时直接停止确认。界面可把自动识别的结果作为初始建议,允许操作员手动纠正,再按相同约束重新检查。若源文件含有两个都叫“物料编号”的列,第一篇的表头检查就应该阻断;即便不同别名同时指向materialCode,这里也会识别到多列候选。额外的来源字段可以保留在预览里,但不能在没有明确业务字段定义时写进核心材料对象。
错误记录不能拖垮一份格式正常的清单
当表头已被确认,逐行校验的目标是让有问题的材料留在隔离清单中,让其余记录继续接受检查。示例清单里有三条数据:MAT-001的DN50不锈钢弯头数量为12,MAT-002的DN20截止阀数量为8.5,MAT-003的电缆桥架数量填写为“八箱”。第三条应被标记为数量格式不符,而不是让前两条一起报错,也不是把“八箱”静默改成零。业务对数量的精度约定为最多三位小数,要求值大于零且不超过100000;单位可选择个、米、套、箱;施工区域必须属于当前工程的允许范围。
每条原始数据要保存来源记录序号、源列数组以及一个稳定的文件指纹引用,不在UI里只留下“第3行失败”这种可能随排序失效的信息。CSV记录序号与文本物理行号不完全相同:被引号包围的字段内部可以有换行,因此界面应优先显示“第3条数据记录”,有解析器提供的物理行上下文时再附上行号。错误对象还应指向目标字段,便于在映射页面突出数量列。
要解决的问题:数量、必填项和区域应分别给出理由
错误隔离不是把所有异常扔进catch,而是每条记录经由确定顺序的字段校验生成问题清单。需要注意材料编码属于字符串,不能被自动类型转换破坏;数量转换之前必须检查十进制格式,科学计数法、千位逗号与含单位的数字默认不接受,避免供应商格式差异造成金额或计量偏差。校验函数不需要读数据库,只接收当前区域集合,便于后续对不同项目复用。
const ALLOWED_UNITS = new Set(['个', '米', '套', '箱']);
const ALLOWED_ZONES = new Set(['东区一层', '西区二层']);
function validateMaterial(row, columnMap) {
const value = key => String(row[columnMap[key]] ?? '').trim();
const item = {
materialCode: value('materialCode'), materialName: value('materialName'),
specification: value('specification'), unit: value('unit'),
quantityText: value('quantity'), zone: value('zone')
};
const errors = [];
if (!/^[A-Z0-9-]{3,24}$/.test(item.materialCode)) errors.push('物料编码格式');
if (!item.materialName || item.materialName.length > 80) errors.push('物料名称长度');
if (!item.specification || item.specification.length > 80) errors.push('规格型号长度');
if (!ALLOWED_UNITS.has(item.unit)) errors.push('计量单位');
if (!ALLOWED_ZONES.has(item.zone)) errors.push('施工区域');
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,3})?$/.test(item.quantityText)) errors.push('数量格式');
const quantity = Number(item.quantityText);
if (!Number.isFinite(quantity) || quantity <= 0 || quantity > 100000) errors.push('数量范围');
return { item: { ...item, quantity }, errors: [...new Set(errors)] };
}
这个结果一方面满足页面上逐字段高亮,另一方面避免使用全局异常控制正常坏行。在区域字典更新后,旧文件重新导入应该按照当前项目的字典版本校验,不能沿用上一轮缓存结果直接提交。精度也要在写入层保持一致:三位小数可选择存储为整数千分单位,或使用明确的小数契约,不应把业务上需要精确比较的计量值交给无约束浮点运算长期累积。
结构错误也要进入可理解的隔离列表
第一篇允许解析器返回不等长的二维数组,是为了不丢失错误上下文。结构校验发生在列映射后,对每一条数据记录单独检查列数。列数不等的记录不能继续依据索引读取材料字段,否则本来属于施工区域的数据可能被误判成数量。隔离行中记录具体列数与预期列数,保持原始值供人工修正;如果CSV引号语法损坏则在更早阶段阻断整个文件,此时没有可靠的记录边界,所谓“跳过坏行”反而会让剩余数据错位。
导入批次中的有效行与隔离行必须有独立计数,业务确认时展示acceptedCount与rejectedCount。如果有效行是两条、隔离行是一条,操作员应该能先导入两条并导出错误明细进行修正,也可以取消整批提交。这里不能悄悄把所有不合格行删掉后显示“导入成功”,因为材料供应链需要知道原清单还有多少内容没进入业务表。
function inspectRows(matrix, columnMap, validate) {
const accepted = [];
const rejected = [];
const expected = matrix[0].length;
for (let i = 1; i < matrix.length; i++) {
const row = matrix[i];
const recordNumber = i;
if (row.length !== expected) {
rejected.push({recordNumber, reason: '列数不一致', raw: row});
continue;
}
const outcome = validate(row, columnMap);
if (outcome.errors.length) {
rejected.push({recordNumber, reason: outcome.errors.join('、'), raw: row});
} else {
accepted.push({recordNumber, data: outcome.item});
}
}
return { accepted, rejected };
}
这里没有在循环中执行写库。导入过程先构建一份只读预览结果,用户确认之后,再由事务层按确定规则提交accepted。同一文件重试导入时还要有文件指纹和业务主键核查,防止网络响应迟到后重复写入。隔离行应有保留期限,不能因为暂时显示在页面上就被视为长期业务档案;更不能通过列表“删除错误行”操作回写修改用户选取的原始CSV文件。

页面交互与存储提交之间还有一个确认关口
HarmonyOS侧可以用左侧列映射表、右侧样本预览的双栏布局处理大屏,手机侧则先展示字段映射卡,再切到隔离详情。两端都使用相同的标准字段和业务规则,不应因为版式不同而得出不同的导入计数。用户选择新文件时将原预览标记失效,确保不会把上一份清单的确认按钮误用到新文件。确认请求携带表头指纹、源文件摘要和映射版本,在提交瞬间重新核对,降低页面长期停留引起的配置漂移。
遇到重复编码要结合规格和施工区域确定业务键,不要直接根据显示名称覆盖记录。不同供应商可能给同一材料使用不同名称,业务入库需要单独的去重策略,而不是把列映射和主数据合并混为一个过程。错误记录的导出同样要防止电子表格公式注入:以=、+、-、@开头的外部文本写回表格前,应采用安全的单元格序列化规则;清单解析阶段仍保留原值用于调查,不能简单剥掉符号后写入主库。
调试时建议按表头映射、结构分布、业务错误和提交事务四步查看。如果“数量”列全是非法字符串,先检查是否选错列,不要立刻放宽数量规则;如果两条有效记录的数量变成零,检查是否在语法解析阶段开启了类型转换;如果某条包含逗号的名称造成列数不齐,回到第一篇检查引号处理。错误行数突然减少却没有任何修正记录时,检查有没有在上游开启自动跳过异常记录。只有每一步都有可追溯的输入输出,才谈得上批量导入的可维护性。

从这一篇开始,材料导入结果不再是一个含糊的“成功或失败”,而是清楚地区分通过校验的材料、需要修正的记录和无法确定结构的整份文件。下一阶段面对更大清单时,仍可以沿用相同的映射版本、稳定记录序号及隔离格式,只把处理方式换成分段执行和可续接的进度记录。
相关官方开发资料可参阅DevEco Studio 的 oh-package.json5 配置说明和OHPM 三方库中心仓;CSV字段切分、记录信息与字段数量处理参阅csv-parse 配置清单及同步解析接口。页面层负责确认与预览,业务校验和持久化应保持独立模块。
更多推荐

所有评论(0)