LedgerDock前七篇已经把CSV导入中比较难缠的事情拆开处理:引号里的换行要留在同一条记录里,坏金额要保留行列位置,金额以整数分计算,重复文件通过指纹拦截,分块解析时界面进度不能跟着每个字符刷新。最后要补的不是另一种解析算法,而是把核对完成的结果交出去,同时确保本地HAR模块升级后旧数据还能读取。

当前账单依然是四条:TX001物料12500分、TX002配件8600分、TX003差旅金额12.5x、TX004办公4300分。前三条有效记录指的是TX001、TX002和TX004,有效合计25400分;目标对账金额27000分,差额为目标减去有效合计,即+1600分。TX003应留在错误清单,不能被偷偷转成1250分,更不能参与总额。导出文件只包含通过校验的三笔;错误明细仍能从导入批次页面追踪。

账单对账结果导出与HAR数据契约升级的处理路径

导出时先固定语义,再考虑文件格式

一个常见失误是把页面里格式化过的“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导出函数与版本迁移的模块关系

写入文件必须区分沙箱与用户目录

页面拿到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并维护文件描述符,写入完毕后统一关闭。文件落地完成才更新界面的成功状态,保存失败则让用户重新选择目标路径或重试。若文件名可能在极短间隔内重复,业务层还需生成受控的唯一文件名,避免意外覆盖同名旧文件。

三条有效记录的CSV导出结果与对账汇总

给这套对账流程留下可回查的记录

回归检查可以从前几篇的同一组示例数据开始:先导入四条记录,确认TX003仍然作为金额错误留在诊断列表;将三条有效记录导出后用增量扫描器读回,业务记录数应还是三条,TX002说明内的换行也应保持完整。再拿一份第一版账本通过upgradeLedger加载,检查有效合计仍为25400分,目标27000分、差额+1600分没有受到迁移影响,同时对缺失的旧错误明细给出可见提示。

本地HAR的价值不是多了一层目录,而是把字段转义、类型迁移和持久文件写入划分为可单独检查的职责。CSV是便于交换的文本结果,账本存档负责结构化事实,导入错误记录负责追踪缺陷,三个东西不要合并成一份含糊的“导出成功”状态。这样即使后续更换UI组件、更新库版本,历史金额和导入批次的解释仍然有明确依据。

相关实现资料可查阅华为开发者文档:开发静态共享包HAR、模块级oh-package.json5配置、添加依赖项、Core File Kit目录访问与写入示例。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐