ArkData + HarmonyOS 能耗账本收官:读写一致性与多端回归 · 04【共创稿事节】
EnergyNest 的最后一道工程题,已经不在七日柱状图的配色上。假设用户在周日把 7.8 kWh 更正为 7.1 kWh,列表立刻显示新值,但顶部周总量仍是旧数字;或者用户查看“异常模式预览”后退出,下次打开却发现预览数据写进了真实账本。这两种情况都比一根柱子的视觉误差严重:界面看起来合理,数据却在不同环节产生了分歧。前面三篇分别完成了响应式看板、以日期为主键的 ArkData 落盘,以及缺失日、零值、峰值的语义解释。收官篇不重新发明存储方案,而是把它们串成一条可以检查、可以复现、出现异常时也能说清原因的完整链路。

一、确定一份贯穿读写和界面的质量契约
完整链路可以写成“用户修订 → EnergyRepository.upsert() → energy_day 表 → EnergyRepository.list() → inspectWeek() → ArkUI”。数据库里仍然保存整数 Wh,表结构保持 day_key TEXT PRIMARY KEY、wh INTEGER、source TEXT 和 updated_at INTEGER;视图中才换算成 kWh。updated_at 表示写入时间,而不是电表实际采样时间,这个语义不能在收尾时突然改变。界面永远消费一次查询得到的同一份 EnergyPoint[],不允许摘要卡单独使用临时数组,柱状图再查询一次数据库。否则两次读取恰好跨过修订时刻,就可能出现“总量和柱子不一致”。
本章保留第三篇提供的 EnergyRecord、EnergyPoint、WEEK_DATES、inspectWeek()、observedCount() 和 sumObservedWh(),也保留只在内存里使用的 previewRecords。这组预览故意让 11 月 4 日缺失、11 月 5 日为有效的零值、11 月 7 日出现 28600 Wh 的待核对峰值,六条实际记录相加为 54500 Wh。因此显示“已记录 6/7 天”“已记录总量 54.5 kWh”是确定的。它不是当前家庭的真实表计读数,也不能把缺失日用零填补后再声称采集完整。正常账本的七条样例则为 4800、5600、6100、7200、8400、7900、7100 Wh,总量 47100 Wh;两组数据必须通过清晰的数据来源标识隔离。
设计评审时,我把验收条件收敛成四条:同日写入只能留下一个日期主键;查询失败时旧快照仍可读,不悄悄清空;缺失、零值和峰值同时进入摘要与明细;手机与大屏的布局可以重排,但同一份输入必须产生相同统计结果。这里最重要的不是“有四个检查项”,而是每一项都有明确的输入、输出和失败提示。能把“缺陷”描述成具体数据契约,才有条件编写有意义的回归用例。
二、把质量检查从 UI 中提取出来
上篇的 inspectWeek 已经负责将数据库行补齐为七个日历位置。现在新建 entry/src/main/ets/model/WeekAudit.ets,只接收整理后的数组,输出一份摘要。此处不依赖 UIAbility、RdbStore 或页面生命周期,因此可以用纯函数测试。null 表示未采集,0 表示真实记录为零;统计分母仍以已有记录天数为准。待核对峰值加入总量,但单独计数,绝不因为异常标签擅自把原始值从账本删除。
// entry/src/main/ets/model/WeekAudit.ets
import { EnergyPoint } from './EnergyQuality';
export interface WeekAudit {
observed: number;
missing: number;
zeroDays: number;
spikeDays: number;
totalWh: number;
averageObservedWh: number | null;
}
export function auditWeek(days: EnergyPoint[]): WeekAudit {
const present = days.filter((p: EnergyPoint) => p.wh !== null);
const totalWh = present.reduce((sum: number, p: EnergyPoint) =>
sum + (p.wh ?? 0), 0);
return {
observed: present.length,
missing: days.filter((p: EnergyPoint) => p.flag === 'missing').length,
zeroDays: days.filter((p: EnergyPoint) => p.flag === 'zero').length,
spikeDays: days.filter((p: EnergyPoint) => p.flag === 'spike').length,
totalWh: totalWh,
averageObservedWh: present.length === 0 ? null : totalWh / present.length
};
}
export function formatKwh(wh: number): string {
return (wh / 1000).toFixed(1);
}
averageObservedWh 返回 null 而不是零很重要:七天没有任何采集记录时,平均值根本无从计算。用 0 作为默认值,读者容易误以为整周真的没有耗电。使用者看到的文字应该是“暂无有效记录”,而不是“日均 0.0 kWh”。另一方面,spikeDays 表示超过上一篇示例阈值 18000 Wh 的日期数量,不是故障次数。真实生产场景应当允许按设备基线、采样完整度和用户反馈调整阈值,不能直接将实验规则作为通用用电安全判断。
在此层还有一个值得防守的边界:既然 EnergyPoint[] 本来就由 WEEK_DATES 生成,按设计应恰好有七个元素。但纯函数并不能假设所有调用者始终正确;测试还要检查传入空数组时平均值为 null,以及只有一条零值时“已记录 1 天、总量 0 Wh”。对于正式服务,可以在仓库边界核验日期字符串是否是合法日历日期;前几篇用的简单正则只保证外观如 YYYY-MM-DD,并不能自动排除不存在的日子。
三、一次加载生成一份快照,避免异步响应覆盖
从本地数据库加载时,应当只从已经初始化的 EnergyRepository 取得列表,再统一构建整理数据和统计摘要。若用户在前一次查询还没结束时点击了“刷新”,旧查询完成得更晚,就可能反过来覆盖新结果。这个问题与城市活动目录的网络竞态不同,却共享一个原则:只有最后发起的加载结果可以提交到页面状态。下例展示 QualityPage.ets 中可加入的核心逻辑,保留上一章的 EnergyPoint 模型和数据库仓库,而不重复建表。
// entry/src/main/ets/pages/QualityPage.ets(增量实现)
import { hilog } from '@kit.PerformanceAnalysisKit';
import { EnergyPoint, inspectWeek } from '../model/EnergyQuality';
import { WeekAudit, auditWeek } from '../model/WeekAudit';
import { EnergyRepository } from '../data/EnergyRepository';
// 以下成员加入现有 QualityPage 组件内部
@State private days: EnergyPoint[] = [];
@State private issue: string = '';
@State private preview: boolean = false;
private loadSerial: number = 0;
private repository: EnergyRepository = new EnergyRepository();
private async refreshFromStore(): Promise<void> {
const serial = ++this.loadSerial;
try {
const records = await this.repository.list('2026-11-02', '2026-11-08');
if (serial !== this.loadSerial) { return; }
const next = inspectWeek(records);
const report: WeekAudit = auditWeek(next);
this.days = next;
this.preview = false;
this.issue = '';
hilog.info(0x1200, 'EnergyNest',
'week loaded, observed=%{public}d missing=%{public}d',
report.observed, report.missing);
} catch (e) {
if (serial !== this.loadSerial) { return; }
this.issue = '读取失败,仍显示上一次数据,可重试';
hilog.warn(0x1200, 'EnergyNest', 'week read failed');
}
}
这个片段是增量接口,不应与第三篇中原有的 @State days 成员重复声明。repository.open(this.getUIContext().getHostContext()...) 的初始化方式取决于项目如何取得 UIAbilityContext;沿用第二篇已经实现的初始化顺序即可,必须先 await open() 再调用这里的 refreshFromStore()。当 issue 不为空时页面可显示重试按钮,不主动清空 days。旧快照保证故障期间能阅读,但界面也必须明确告知这不是最新值。hilog 只记录已记录天数与缺失数量,不把用户地址、设备标识和原始明细整段输出到日志。即使应用的数据量很小,日志也应先有隐私边界。

四、让窄宽屏展示同一个结论
第一篇使用 GridRow 与 GridCol 实现了布局重排。收官时最容易引入的回归是把“宽屏双列”变成“每列自己重新算一次总量”。更稳妥的做法是:仓库只读一次,质量整理只做一次,summary 和 chart 两个子组件都消费同一份 days 与 auditWeek(days) 结果。窄屏把摘要置于柱状图上方;宽屏让摘要卡和日期明细并排;两种布局在日期和数值上必须完全一致。具体断点应依照实际设备窗口与页面内容测试,不能仅凭设备型号猜宽度。
此外,当用户打开“异常模式预览”时,只将内存中的 days 替换为 inspectWeek(previewRecords) 并设置 preview=true。页面顶部显示“异常模式预览 · 不修改账本”;点击“返回账本”时重新查询数据库。这个动作不是写回,更不能把 previewRecords 交给 upsert()。数据库查询发生错误时,保留原来的 preview 标识,避免把预览图误标成真实账本。对于空周,七个日期位置仍然显示“缺失”,但不要画七根零高度柱并标注“已记录七天”。
一个简单却有效的人工验证方法是在窄屏依次点开周三、周四和周六:周三应显示“缺少记录”;周四应显示“已记录 0 Wh”;周六应显示“28.6 kWh,建议核对”。把同一页面切换到宽屏,文字语义与 54.5 kWh 的总量不能改变。还可以把页面的文字缩放级别调大,检查数值是否截断、风险说明是否被按钮覆盖;这些属于响应式和可读性验收,不应被单纯的截图像素对齐代替。

五、用可重复的回归样例收尾
最后建立三组独立的验收样例,而不是只看一张“页面正常”的截图。第一组测试日期唯一键:先写入 11 月 8 日 7800 Wh,再把同日改成 7100 Wh;查询应只返回一条记录,周总量按照新值重算。这里要检查数据库原始行数,不能只检查 UI 因为列表去重恰好隐藏了重复行。第二组测试数据语义:将预览样本交给 inspectWeek() 和 auditWeek(),结果必须为七个日历位置、六个已记录日、一个缺失日、一个零值日、一个峰值日、总量 54500 Wh。第三组测试故障恢复:令仓库查询失败,检查页面保留旧快照、出现重试入口,再恢复可用数据库重新加载。
这些测试要分清层级。纯函数的断言能够验证统计公式,但不能证明数据库事务已经提交;数据库构造数据的查询能够验证主键与关闭游标的策略,却不能证明折叠屏上的布局可读;开发工具的预览可以帮助检查排版,但不能代替不同窗口尺寸、字体级别以及真机触控的交互验证。将它们组合起来,才是合理的工程验收矩阵。尤其是涉及日志时,只记录脱敏的状态计数和失败类型;需要排查具体异常的详细信息应遵守环境的日志策略,不能为了方便定位把用户数据整行打印出来。
本系列使用了 ArkUI V1 的 @Component、@State,ArkData 的 RdbStore,以及 GridRow 响应式栅格;只使用公开可查的接口,不需要额外网络权限。数据库损坏、迁移、跨设备合并与数据备份属于更大范围的工程主题,不能因为这一套本地七日样例运行逻辑可解释,就把它当成完整能源平台。正式接入采集设备时,日期必须结合用户所在地时区确定,来源字段也要通过可信采集链路校验。日期唯一键只保证同一天落成一行,并不自动保证那一行采集值正确。
六、从一张图表回到可信的数据链
四篇连载完成后,EnergyNest 的职责边界已经相对清楚:GridRow 管理布局,不改变统计定义;EnergyRepository 管理读写与日期唯一性,不自行裁决异常;inspectWeek() 管理缺失、零值和峰值语义;auditWeek() 管理可重复统计;ArkUI 只展示同一快照与用户选择的状态。这种拆分不会让代码神奇地免于故障,却能帮助团队在出现“某一天数字不对”时迅速找到具体层级,避免把所有责任推给图表组件。
工程上的最终判断不是“柱状图足够漂亮”,而是数据写入后能读回、读回后能解释、解释后能在不同设备上保持同一结论,并且在任何一步失败时让用户知道看到的是否为最新数据。若未来继续扩展为月度报表、真实设备接入或多端同步,可以沿用这条质量契约,再逐步增加版本迁移、采集可信度和端间冲突处理,而不用推倒这一套已经清晰的本地账本。
参考文档:华为 RdbStore 查询与存储接口、ResultSet 使用与关闭、GridRow 响应式栅格、HiLog 日志接口。
更多推荐

所有评论(0)