19|HarmonyOS材料CSV分批入库与中断续接【共创稿事节】
工地材料导入工作台进入第三个环节后,问题从“如何识别一行数据”转向“已确认的几百行怎样在中断后保持准确”。前两篇已约定六个业务字段:materialCode、materialName、specification、unit、quantity和zone。输入经过CSV语法解析、列映射及数量校验后,既有有效材料,也有需要单独保留理由的错误记录。清单从几百条增长到上万条时,一次性构建完整数组再写库,会使页面响应、内存峰值与数据提交状态纠缠在一起。
更值得处理的是中途退出:假定已将前400条业务记录写入存储,操作员切到别的页面,下一次应从第401条接着提交,而不是从头插入400条造成重复;若文件本身被替换,旧进度也不应继续套用到新内容。这里把分页展示和真正的提交边界分开,建立文件身份、逻辑记录序号、批次事务和续接游标四个互相校验的部分。用户在HarmonyOS页面看到的是进度与问题条数,批次调度和数据库写入则由清晰的业务接口负责。

以完整文件身份锁定一次导入
单凭文件名site_materials.csv或文件大小定位来源远远不够。工程人员可以用同名文件覆盖原清单,也可以把供应商文件复制到另一目录;这两种操作都不能让旧断点变成新文件的合法断点。我采用SHA-256摘要对原始字节进行标识,并把本次列映射、校验规则版本、解析策略版本组合成一次导入身份。只要影响业务解释的部分变化,就派生不同的导入身份并重新审核。摘要计算采用流式读取,避免单为指纹而把大文件额外完整载入内存。
文件哈希只标识内容的一致性,不能替代来源权限与文件安全检查。业务还要保留操作人员选择的来源名称、文件总字节数和业务导入时间,但这些信息不参与对同一内容的判定。映射必须按固定字段键排序;如果只是对象属性写入顺序变化,产生不同哈希会导致没有意义的重复导入。在页面上显示摘要前后若干位即可,完整摘要留给存储层使用。
要解决的问题:同一内容映射改变却沿用旧游标
const { createHash } = require('node:crypto');
const { Readable } = require('node:stream');
const assert = require('node:assert/strict');
async function sha256Stream(stream, maxBytes = 20 * 1024 * 1024) {
let total = 0;
const digest = createHash('sha256');
for await (const bytes of stream) {
total += bytes.length;
if (total > maxBytes) throw new Error('文件超出流式入口容量');
digest.update(bytes);
}
return digest.digest('hex');
}
function importIdentity(fileHash, mapping, parserVersion, validatorVersion) {
const ordered = Object.fromEntries(Object.entries(mapping)
.sort(([left], [right]) => left.localeCompare(right)));
return createHash('sha256').update(JSON.stringify({
fileHash, mapping: ordered, parserVersion, validatorVersion
})).digest('hex');
}
(async () => {
const fileHash = await sha256Stream(Readable.from([Buffer.from('材料编号,数量\nMAT-001,12')]));
const map = { quantity: 1, materialCode: 0 };
const a = importIdentity(fileHash, map, 'csv-v1', 'material-v2');
const b = importIdentity(fileHash, { materialCode: 0, quantity: 1 }, 'csv-v1', 'material-v2');
assert.equal(a, b);
assert.notEqual(a, importIdentity(fileHash, map, 'csv-v1', 'material-v3'));
console.log('identity stable:', a.slice(0, 12));
})();
这里刻意把身份计算放在业务层,而不依靠页面的临时状态。任务被中断后,续接前重新读取原始文件并计算摘要,只有文件、映射与规则版本全部吻合时才能使用已经保存的进度。若文件已经丢失,保留断点供审计查询,但要求重新选择来源;如果摘要不符,必须新建一次导入,不允许仅凭行号强行恢复。
切片按逻辑记录而不是文本物理行
CSV有被双引号包围的换行。比如一条材料备注跨了两行,文本中的第401行未必等于第401条材料记录。第一篇使用解析器决定记录边界,第三篇仍沿用同一约定:表头之后的数据记录从1开始连续编号,错误记录同样占据一个编号。续接游标描述“下一条尚待提交的逻辑记录”,不能描述文件指针随意跳过的字节偏移。文件流每次重新扫描到已完成的记录边界,然后从下一个逻辑记录组织批次,才能避免在引号内部截断。
前两篇采用单文件2 MiB、数据记录5000条的同步预览入口;此处为大清单增加独立的流式入口,例如以20 MiB和50000条作为可配置的业务上限。容量检查仍从原始字节流开始,记录数由解析器逐条计数;更换入口后不得沿用旧的入口配置摘要。图片里的2.8 MB清单属于流式入口范围,和小文件入口的约束并不矛盾。
批次大小选择200条只是一个便于说明的业务参数,不是性能结论。它可以按实际数据量与事务成本调整,但一次导入的顺序必须固定。上游解析器持续产出一条记录,下游列映射返回接受或隔离的结果,切片器只负责将结果按原始顺序合并,不重新排序,也不忽略错误条目。将错误记录完全丢弃,会造成游标向前移动却无法解释缺少的材料记录。
要解决的问题:保留输入顺序并且不一次性缓存所有数据
const assert = require('node:assert/strict');
async function* chunkRecords(records, size) {
if (!Number.isSafeInteger(size) || size < 1) throw new Error('无效批次大小');
let batch = [];
for await (const record of records) {
batch.push(record);
if (batch.length === size) {
yield batch;
batch = [];
}
}
if (batch.length) yield batch;
}
async function* mappedRows() {
yield { recordNo: 1, status: 'accepted', code: 'MAT-001' };
yield { recordNo: 2, status: 'accepted', code: 'MAT-002' };
yield { recordNo: 3, status: 'rejected', reason: '数量格式' };
yield { recordNo: 4, status: 'accepted', code: 'MAT-004' };
yield { recordNo: 5, status: 'accepted', code: 'MAT-005' };
}
(async () => {
const parts = [];
for await (const part of chunkRecords(mappedRows(), 2)) parts.push(part);
assert.deepEqual(parts.map(x => x.length), [2, 2, 1]);
assert.equal(parts[1][0].status, 'rejected');
assert.deepEqual(parts.flat().map(x => x.recordNo), [1, 2, 3, 4, 5]);
console.log('chunk order:', parts.map(x => x.length).join('/'));
})();
流式解析可按csv-parse提供的Node.js Transform流方式接入:源文件的ReadStream向解析器提供字节,解析器产出标准记录,再进入上述切片器。这里的切片算法不需要了解CSV内部如何转义,更不会尝试使用字符串分割来处理换行。业务运行于Node预处理环境时可直接使用Node流;迁移到HarmonyOS ArkTS侧时,保持相同记录契约,通过适配层连接目标环境支持的文件接口与存储接口。

事务边界让材料记录与进度一起提交
不能先把断点写成“已完成600条”,随后才保存第401至600条数据。两次写入之间任何异常都会造成进度跑在材料数据前面。正确的数据库操作顺序是:开始事务、写入本批所有记录及隔离结果、更新续接位置,然后一起提交。一个业务记录用importId + recordNo形成唯一键,这样同一批在异常后重新提交也不会复制数据。隔离结果同样写入事务,nextRecord始终反映已经处理的连续记录范围,而非仅成功入库的条数。
具体存储实现分清平台:下面使用Node.js当前运行环境的node:sqlite来表达和检查事务语义;HarmonyOS应用侧的关系数据调用应通过ArkData存储适配器接入,不把Node内建模块写成ArkTS系统接口。两个环境要保持相同表约束与回滚规则,驱动的函数名称无需相同。
要解决的问题:重复提交与异常中断造成双写或跳行
const { DatabaseSync } = require('node:sqlite');
const assert = require('node:assert/strict');
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE import_row (
import_id TEXT NOT NULL, record_no INTEGER NOT NULL,
status TEXT NOT NULL, payload TEXT NOT NULL,
PRIMARY KEY(import_id, record_no)
); CREATE TABLE import_checkpoint (
import_id TEXT PRIMARY KEY, next_record INTEGER NOT NULL
);`);
function commitBatch(importId, records, nextRecord) {
db.exec('BEGIN IMMEDIATE');
try {
const insert = db.prepare('INSERT OR IGNORE INTO import_row VALUES (?, ?, ?, ?)');
for (const row of records) {
insert.run(importId, row.recordNo, row.status, JSON.stringify(row));
}
db.prepare(`INSERT INTO import_checkpoint VALUES (?, ?)
ON CONFLICT(import_id) DO UPDATE SET
next_record = MAX(import_checkpoint.next_record, excluded.next_record)`)
.run(importId, nextRecord);
db.exec('COMMIT');
} catch (error) {
db.exec('ROLLBACK');
throw error;
}
}
const rows = [{recordNo: 1, status: 'accepted'},
{recordNo: 2, status: 'rejected', reason: '数量格式'}];
commitBatch('same-file-and-contract', rows, 3);
commitBatch('same-file-and-contract', rows, 3);
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM import_row').get().n, 2);
assert.equal(db.prepare('SELECT next_record AS n FROM import_checkpoint').get().n, 3);
console.log('checkpoint nextRecord:', 3);
db.close();
这段事务体现了“重复提交不会增加相同行号”的约束。完整工程还应校验批次必须与现有nextRecord接壤,并在批次提交前后核实相同唯一键的内容哈希;不能让更新后的文件携带旧身份覆盖历史记录。对多进程导入,还需通过唯一运行标识和事务锁避免两位操作员同时推进同一个游标。INSERT OR IGNORE适合讲解幂等的核心机制,但不能代替冲突内容比对及权限检查。
恢复时的状态流转与异常归属
导入页面可以把进度拆成待选择文件、待校验身份、准备续接、正在提交、需要处理冲突、已经完成。页面再次打开时,先取到保存的importId与nextRecord,核对源文件内容和映射规则,确认已提交的材料记录、隔离记录与游标一致,再启用“继续导入”。正在执行的批次若遇到取消、应用退出、磁盘空间不足或数据库写入异常,事务回滚后保持原游标;恢复时重新处理该批,唯一键确保不重复。
进度百分比只能用于展示,不应作为恢复依据。未知文件总记录数时可以显示“已处理400条”而不制造一个百分比。按原始记录顺序前进时,成功材料数加隔离材料数才等于本轮已经归档的逻辑记录数。错误日志保留导入身份、逻辑记录号、异常类型、提交边界,不写出整份供应商清单内容。若来源文件失效、校验规则变更或用户撤销导入,都应明确说明是等待确认、创建新运行还是保留历史,而不是悄悄清空已完成数据。

把文件处理与页面生命周期拆开
界面可以订阅一次导入的进度事件,但订阅者离开不应直接决定后台提交是否结束:应由明确的取消指令控制任务取消,由导入协调器在安全边界收尾。页面卸载时解除订阅、终止页面定时刷新,并释放本页预览数组;文件流与数据库句柄归导入协调器管理,完成、取消或异常时统一关闭。用户重进页面通过数据库断点重建视图,而不是依赖上次组件保留在内存中的数组。
对于大清单,我更关注每次事务结果是否可追溯,而不盲目追求一个固定的每秒写入数量。稳定的记录身份、严格递进的游标、与提交绑定的隔离记录和正确的资源归属,才能让续接操作有明确含义。下一篇会把这些规则固化为回归用例与版本锁定要求,避免第三方解析器升级改变记录边界后影响已经保存的断点。
相关接口与开发资料:HarmonyOS ArkTS概述、HarmonyOS工程依赖配置、CSV Parse流式接口。
更多推荐



所有评论(0)