HarmonyOS ArkTS:账单结果导出与HAR数据契约迁移(连载08)【共创稿事节】
LedgerDock前七篇已经把CSV导入中比较难缠的事情拆开处理:引号里的换行要留在同一条记录里,坏金额要保留行列位置,金额以整数分计算,重复文件通过指纹拦截,分块解析时界面进度不能跟着每个字符刷新。最后要补的不是另一种解析算法,而是把核对完成的结果交出去,同时确保本地HAR模块升级后旧数据还能读取。
当前账单依然是四条:TX001物料12500分、TX002配件8600分、TX003差旅金额12.5x、TX004办公4300分。前三条有效记录指的是TX001、TX002和TX004,有效合计25400分;目标对账金额27000分,差额为目标减去有效合计,即+1600分。TX003应留在错误清单,不能被偷偷转成1250分,更不能参与总额。导出文件只包含通过校验的三笔;错误明细仍能从导入批次页面追踪。

导出时先固定语义,再考虑文件格式
一个常见失误是把页面里格式化过的“125.00元”直接拼进CSV。表格软件可以按当前语言环境自动识别小数点、货币符号和日期,但下次导回程序时未必得到相同整数分。导出协议因此固定为流水号、日期、金额分、说明四列,金额列保持十进制整数字符串。币种由账本规则控制,不让单个单元格自行决定如何换算。对账总额与目标金额放在导出结果页,不伪装成第四笔业务记录。
CSV的另一个陷阱是引号和换行。TX002说明字段使用“配件”换行“需复核”的两行文本;它在界面里是一条记录,写文件时依然应该是一个被双引号包裹的单元格。字段内部出现双引号,要写成两个双引号;字段出现逗号、回车或换行,要用双引号围住整个值。拼接出来的物理文本行数可能多于业务记录数,这并不是账目重复。读回时还必须调用第三篇建立的增量扫描逻辑,不可重新退回逐行简单拆分。
让CSV序列化成为HAR的纯函数
要解决的问题是把“记录如何写出来”留在解析组件内部,让页面只负责拿到文本和触发保存。先做字段级转义,再按稳定的流水号顺序输出,保证同一组有效记录获得可预测的导出内容。对可能被表格软件当作公式的说明文本,增加一个面向表格查看的前置转义;该策略应与原始业务数据分开存放,以免再次导入时把转义符误认为原始内容。
// csvparser/src/main/ets/export/CsvExporter.ets
export interface ExportRow {
id: string;
date: string;
amountFen: number;
note: string;
}
function field(text: string): string {
const safe: string = /^[=+\-@]/.test(text) ? "'" + text : text;
const escaped: string = safe.replace(/"/g, '""');
return /[",\r\n]/.test(safe) ? '"' + escaped + '"' : escaped;
}
export function buildExportCsv(rows: ExportRow[]): string {
const valid: ExportRow[] = rows.filter((row: ExportRow): boolean =>
Number.isSafeInteger(row.amountFen) && row.amountFen >= 0);
const ordered: ExportRow[] = valid.slice().sort((a: ExportRow, b: ExportRow): number =>
a.id.localeCompare(b.id));
const lines: string[] = ['流水号,日期,金额分,说明'];
ordered.forEach((row: ExportRow): void => {
lines.push([field(row.id), field(row.date), String(row.amountFen),
field(row.note)].join(','));
});
return lines.join('\r\n') + '\r\n';
}
这里再次校验整数金额,是为了防止调用方绕过导入校验直接传入小数。但“过滤掉不合法金额”和“记下错误原因”是两件事:业务入口在调用导出函数前已经将错误记录单独登记,并展示三条有效、一条错误的批次统计;导出函数收到的本来就应该是有效集合。若调用方误传其他数据,函数不会把坏金额输出到文件,但页面仍要把拒绝数量记录为可见问题,不应该借此改写批次账本。
本次输出列的先后次序与第一篇的输入协议保持一致,可以安排TX002在说明字段内含换行,作为导出后的回读检查。即使文件文本有额外的物理行,buildExportCsv也只写三条合法业务记录。保存前不要给原始数组直接排序,slice()是为了保护页面持有的顺序;文件顺序变化不应触发新的业务指纹或重复入库记录。
将HAR版本和账本数据版本分开
库包版本描述的是组件对外接口的发布节奏,账本的schemaVersion描述的是应用持久数据结构,不能用0.3.0和2互相代替。上一版本账本只存有效列表和目标金额,现在导出页面还需要错误摘要是否完整、导出选项和来源协议。旧数据如果没有错误列表,迁移后就显示“错误明细待复核”,不能凭空补一个错误数为零。
要解决的问题是读取旧账本时提供一个纯函数升级入口,同时保持HAR的公开解析端口不因新增字段而被破坏。下面用判别联合区分旧结构和新结构;升级动作复制列表,保留金额数值,增加是否具备完整错误信息的标记。对于已经是第二版的数据,返回副本而不重复修改。
// csvparser/src/main/ets/migration/LedgerUpgrade.ets
import { ExportRow } from '../export/CsvExporter';
export interface LedgerV1 {
schemaVersion: 1;
accepted: ExportRow[];
targetFen: number;
}
export interface LedgerV2 {
schemaVersion: 2;
accepted: ExportRow[];
targetFen: number;
issues: string[];
issuesComplete: boolean;
}
export function upgradeLedger(raw: LedgerV1 | LedgerV2): LedgerV2 {
if (raw.schemaVersion === 2) {
return { schemaVersion: 2, accepted: raw.accepted.slice(),
targetFen: raw.targetFen, issues: raw.issues.slice(),
issuesComplete: raw.issuesComplete };
}
return { schemaVersion: 2, accepted: raw.accepted.slice(),
targetFen: raw.targetFen, issues: [], issuesComplete: false };
}
迁移函数没有向外界许诺旧版存档可以恢复所有错误行:它做的是明确保存“信息是否齐全”。实际导出前,页面先检查issuesComplete。旧版存档的有效金额仍可导出,但错误追踪区域应提示重新读取原CSV以补齐诊断信息。若目标金额不是安全整数,或升级读取到空流水号、重复流水号,则应中止导出并要求修正数据,不让迁移掩盖先前未处理的脏记录。
HAR的Index.ets继续保留原来的CSV读取端口,把buildExportCsv与upgradeLedger作为新增公开函数导出;对应模块级oh-package.json5按语义化规则设置版本,调用方锁定需要的依赖版本。应用处理一个账本升级过程时,以数据版本判断结构,以库包版本判断可以调用哪些接口,不再依靠“文件名看起来更新”来推断兼容性。依赖调整后,要检查引用路径和导出声明的实际拼写,避免工程内联导入了未公开的内部路径。

写入文件必须区分沙箱与用户目录
页面拿到CSV字符串后,要创建一个真正可读的文件并关闭文件句柄。context.filesDir属于应用可访问的私有目录,适合作为本次导出结果的落地点;保存这里不等于文件已经进入用户的下载目录,也不等于其他应用可以直接打开。需要用户选定外部目录时,应另行接入系统提供的文件选择或授权流程,不能靠拼一个公共路径绕过访问控制。
要解决的问题是即使写入发生异常,也能关闭已打开的文件描述符。示例为每次操作生成不同文件名,不覆盖旧结果;失败时保留原账本与导入批次,不把临时文件当成已完成的导出。
// entry/src/main/ets/export/ExportFileWriter.ets
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
export function writeCsvToAppFiles(context: common.UIAbilityContext,
csvText: string): string {
const path: string = `${context.filesDir}/ledger_export_${Date.now()}.csv`;
let opened: fileIo.File | undefined = undefined;
try {
opened = fileIo.openSync(path,
fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
fileIo.writeSync(opened.fd, csvText);
return path;
} finally {
if (opened !== undefined) fileIo.closeSync(opened);
}
}
对于体积比较大的导出结果,先将所有数据拼接成字符串会额外占用内存。第七篇已经对输入采用分块扫描,输出也可以沿相同思路按行分批写入;每批构造一小段CSV并维护文件描述符,写入完毕后统一关闭。文件落地完成才更新界面的成功状态,保存失败则让用户重新选择目标路径或重试。若文件名可能在极短间隔内重复,业务层还需生成受控的唯一文件名,避免意外覆盖同名旧文件。

给这套对账流程留下可回查的记录
回归检查可以从前几篇的同一组示例数据开始:先导入四条记录,确认TX003仍然作为金额错误留在诊断列表;将三条有效记录导出后用增量扫描器读回,业务记录数应还是三条,TX002说明内的换行也应保持完整。再拿一份第一版账本通过upgradeLedger加载,检查有效合计仍为25400分,目标27000分、差额+1600分没有受到迁移影响,同时对缺失的旧错误明细给出可见提示。
本地HAR的价值不是多了一层目录,而是把字段转义、类型迁移和持久文件写入划分为可单独检查的职责。CSV是便于交换的文本结果,账本存档负责结构化事实,导入错误记录负责追踪缺陷,三个东西不要合并成一份含糊的“导出成功”状态。这样即使后续更换UI组件、更新库版本,历史金额和导入批次的解释仍然有明确依据。
相关实现资料可查阅华为开发者文档:开发静态共享包HAR、模块级oh-package.json5配置、添加依赖项、Core File Kit目录访问与写入示例。
更多推荐

所有评论(0)