ArkData + HarmonyOS 票据归档:存储边界与交付收尾(连载06/06)【共创稿事节】
同样都是“保存”,为什么筛选条件写到 Preferences、票据记录写到 RDB,而票据图片以后还可能落到应用文件目录?读到系列收尾,如果仍把这三种存储视为可以随意互换的工具,前五篇费力建立的异常恢复机制就失去了价值。最后一篇换一种方式,不从单个接口的使用说明开始,而从应用的三类真实数据反推存储选型,并用一个归档操作把全链路串起来。
票据归档助手已经有稳定的配置协议:Preferences 文件名 receipt_filter_settings,新版键 receipt_filter_v2,结构是 FilterPrefsV2{version,onlyPending,category,sort},当前版本为 2,老键 only_pending 保留迁移兼容。第三篇的 ReceiptWriteQueue 串行提交设置;第四篇讨论前后台与已提交状态;第五篇新增 ReceiptRepository,以 receipt_id 作为票据身份,archived=0/1 区分待归档与已归档。现在做的是工程收口,而非把存储重新改写成大对象 JSON。

把三种存储放到同一张评审桌上
Preferences 适合小体积、读写频率有限的应用设置。筛选开关、默认排序属于这种数据;用户修改它们,不会改变任意一张票据的业务事实。关系型数据库 RDB 更适合记录级 CRUD、查询过滤和排序;票据的标题、归档状态、日期以及分类应放在这种结构化记录里。至于原始扫描件或 PDF,一旦引入大块二进制内容,应优先评估文件存储和受控的路径引用,而非把每张图片都编码进偏好键。具体附件方案还要结合访问权限、安全级别、清理策略和备份需求确定。

这并不是“Preferences 不安全、RDB 就一定安全”的比较。存储 API 只能提供相应的数据组织与持久化机制,业务安全仍要核对数据分级、访问边界和备份方案。上一阶段演示的 SecurityLevel.S1 是一种配置示例,不是任何票据业务都能照搬的合规结论。若票据涉及企业身份信息、报销凭证或人员数据,应按系统规定重新评估保护等级及文件保留周期。
这里可以用四个问题快速决策。是否需要按多字段组合查询?需要时优先考虑 RDB。是否只是每个用户的一两个轻量开关?Preferences 足够清晰。是否保存长篇二进制附件?应优先评估应用文件及元数据关联。是否需要跨设备同步?前三种本地存储本身都不等同于云同步,必须另行设计账户、权限、冲突和重试协议。把这些条件说清楚,比只比较 API 调用行数更有用。
串行偏好与数据库更新是两笔不同的账
设想用户正在查看“仅看待归档”,在列表里把 receipt_id = r-017 的票据归档。这个操作应该更新 RDB 中那条票据的 archived 值,随后重新按当前 FilterPrefsV2 查询列表。它不需要改动 onlyPending,也不需要向 ReceiptWriteQueue 塞进一个偏好写盘任务。否则一次业务归档就可能触发本来无关的配置写入,后面发生失败时很难解释究竟哪笔数据没生效。
反过来,用户将排序从 newest 改为 oldest,应该走已有队列对 receipt_filter_v2 做合法值校验、串行 put、必要的 flush,然后重新执行 RDB 查询;不应该为了换个显示顺序修改所有票据的 created_at。排序是视图条件,不是业务历史。两套存储在页面的查询时刻汇合,但它们不构成一个跨存储的数据库事务。
下面的增量方法可以加到第五篇的 ReceiptRepository 内部。沿用已有的 private store: relationalStore.RdbStore | null,只修改已存在票据的归档状态;查询与展示逻辑继续调用第五篇的 list() 方法。
// 增加到 ReceiptRepository 类中,继续使用此前的 relationalStore 导入
async setArchived(id: string, archived: boolean): Promise<boolean> {
if (this.store === null) { throw new Error('数据库尚未打开'); }
if (id.trim().length === 0) { throw new Error('票据编号不能为空'); }
const where = new relationalStore.RdbPredicates('receipt');
where.equalTo('receipt_id', id);
const values: relationalStore.ValuesBucket = {
archived: archived ? 1 : 0
};
const changed: number = await this.store.update(values, where);
return changed === 1;
}
如果返回零行,应该解释为记录不存在或已被其他操作删除;界面需要刷新并告知用户,不能直接显示“归档成功”。如果更新调用抛出异常,也不能先把卡片变为已归档再悄悄吞掉异常。数据库内部的事务能力与本地偏好写入队列各管各的数据,不能通过简单地在一个 try 里依次调用两者就宣称具有跨存储原子性。
页面协调层要显式区分两类失败
下面把前一篇的 ReceiptScreenService 扩展出一个较小的归档入口。这里假设应用启动时已经执行 ReceiptRepository.open(),并将仓库实例注入协调层,避免重复创建数据库连接。FilterPrefsV2 和 ReceiptRepository.list() 沿用此前的数据定义;协作服务本身是我们写的业务类,不是系统 SDK 模块。
import { ReceiptRepository, ReceiptRow } from './ReceiptRepository';
import { FilterPrefsV2 } from './ReceiptFilterStore';
export interface ArchiveViewResult {
rows: ReceiptRow[];
message: string;
}
export class ReceiptArchiveCoordinator {
constructor(private readonly repo: ReceiptRepository) {}
async archiveAndRefresh(id: string, filter: FilterPrefsV2):
Promise<ArchiveViewResult> {
const changed: boolean = await this.repo.setArchived(id, true);
if (!changed) {
return { rows: await this.repo.list(filter, 30, 0),
message: '票据不存在,已重新加载列表' };
}
try {
const rows: ReceiptRow[] = await this.repo.list(filter, 30, 0);
return { rows: rows, message: '归档已保存,列表已更新' };
} catch (error) {
// 写入成功后查询失败,不能把写入结果误报为失败
return { rows: [], message: '归档已保存,列表刷新失败,请重新加载' };
}
}
}
这段流程强调一个容易忽视的事实:业务写入成功,界面查询仍可能失败。这时把提示写成“归档失败”会误导用户再次提交;更恰当的是告诉他记录已保存、列表尚待刷新。反之,如果 setArchived() 本身失败,异常要在页面入口捕获并显示可重试反馈,不能套用“已经保存”的提示。真正的页面组件还应保留更新前的行,不应因为这里的结果是空数组就直接清空屏幕。示例把分支返回为纯对象,是为了使状态语义可以被单元测试核对。

另外,读取偏好失败时的兜底条件与票据读取失败时的兜底条件不同。前者可以考虑给出合理默认筛选并提示用户复核,但不能擅自覆盖未知新版配置;后者不能因为查询失败就假装数据库没有票据,否则可能把“暂时不可读”误判成“业务空数据”。若第二篇迁移得到 repair 状态,应先让用户处理或明确恢复,保持第三篇队列的写入保护,不能为赶进度直接清除旧键。
用四条回归主线完成整套系列
第一条是配置迁移:准备旧字段 only_pending=true,在新版配置缺失时执行读迁移,确认筛选仍符合用户预期;再把新版 JSON 故意改为非法 sort 值,应进入校验失败分支而不是随意写回默认值。第二条是连续操作:分别从两个界面触发筛选修改,核对所有请求是否经过同一个 ReceiptWriteQueue 实例;失败任务之后,新任务仍应能继续提交。
第三条是前后台恢复:在偏好变更排队时把应用切到后台,再回到前台核对 committed、pending 与页面 view;不要把生命周期回调当成唯一的保存机会。第四条是业务查询:先按 category='all' 查询,再开启 onlyPending,然后将一条票据归档,确认它从待归档视图中消失,而数据库记录仍然存在;切回“全部”时,它应该以已归档状态可见。最后人为制造一次查询错误,确认不把它显示成“空数据已同步”。
测试准备阶段可以使用固定的演示票据 r-017、r-018 和 r-019,但不要把演示数据条数、运行时长或内存开销写成真机实测。若要开展稳定性评测,应记录设备和系统版本、构建模式、数据规模、步骤及每次可观察的返回值,再将异常代码对应到官方接口文档。不同存储的选型不靠一次主观的“感觉快”,而靠职责、负载与恢复边界是否匹配。
全系列到这里形成清楚的链路:Preferences 保存筛选偏好,旧字段迁移与新结构校验维护可升级性;串行队列负责同一偏好在应用进程内的写入顺序,生命周期恢复避免覆盖已提交状态;RDB 保存票据事实,页面按偏好生成受控查询。大型附件、云端同步和跨进程共享属于下一阶段需求,不在本地归档助手的现有能力范围内。把范围讲清楚,工程才真正可以移交给下一位维护者。
参考华为官方文档:Preferences 用户首选项、关系型数据库谓词接口、HarmonyOS 开发者文档中心。
更多推荐

所有评论(0)