同一张工单在左边列表里被选中,右边是可以编辑的复核意见。折叠屏展开后,这种双栏组织方式很顺手。但界面上始终能看见某条工单,并不意味着当前输入框里保存的就是数据库最新版本。最容易被忽略的情况是:右栏已经保存过一次,另一个旧页面或延迟回调随后又将旧草稿写了回去。本文不讨论平行视界系统接口的具体路由协议,而是把多栏交互里的数据一致性独立拿出来。演示项目叫 SplitDraft,目标是在本地 RelationalStore 中通过 revision 进行条件更新,阻止旧编辑快照覆盖新值。这比“保存按钮点完变绿”更接近可以验收的工程约束。## 一、先分清“当前选中”和“当前可写”示例固定任务 DR-210、工单 RG-208。初始草稿版本是 revision=3,内容为“检查照片角度”;右栏编辑者补写“补齐复核依据”,保存后期望升到版本 4。随后模拟一个携带旧版本 2 的写入请求,它应得到 CONFLICT,不得覆盖版本 4 的文本。页面上用 SAVED_REV4 表示最终状态,演示计数是“已保存 1、冲突阻断 1”。这些数字是测试用例输入和预期输出,不是我的实机测量结果。这套演示故意不使用“左栏第几个”作为持久化主键。折叠屏切换成单栏时,列表排序、过滤、分页都可能改变位置;数据库里认识的应该是 ticket_id。编辑版本也不能跟着 UI 页面重建从零开始,否则重开一个窗口就可能让一个旧表单重新拥有覆盖权。业务上至少需要三个值:ticketId 表示写哪条记录,expectedRevision 表示编辑者基于哪个版本开始修改,body 是准备保存的新文字。相比全局 isSaved 布尔值,版本字段能解释“这次失败究竟是不是别人先改过”。它仍不是完整的多人协同协议,只是本地条件写入的一道门禁。## 二、数据库先约束身份,再谈保存成功RelationalStore 适合结构化草稿,因为项目确实要按照工单 ID 查询,并在同一张表里检查修订号。单个偏好键虽然也能保存一段文本,却没有这里展示的行级条件更新语义。安全级别不应照抄示例数字用于敏感生产数据,真实业务要按照数据分类和系统要求另行设计。### 1. 最小表结构第一段代码要解决“表不存在”和“主键不稳定”两个问题。getRdbStore 根据 Stage 上下文获得数据库对象,executeSql 创建业务表。为便于复现,示例允许测试夹具在首次使用时插入工单 RG-208 的 revision=3 初始值;生产环境应由真实工单创建流程完成这一步。arktsimport { relationalStore } from '@kit.ArkData';class DraftRepository { private store?: relationalStore.RdbStore; async open(context: Context): Promise<void> { const cfg: relationalStore.StoreConfig = { name: 'split_draft.db', securityLevel: relationalStore.SecurityLevel.S1 }; this.store = await relationalStore.getRdbStore(context, cfg); await this.store.executeSql( 'CREATE TABLE IF NOT EXISTS DRAFT (' + 'ticket_id TEXT PRIMARY KEY, body TEXT NOT NULL, ' + 'revision INTEGER NOT NULL)' ); }}这里的数据库文件名是本次 Demo 的自定义名称,不是系统固定路径。CREATE TABLE IF NOT EXISTS 负责多次进入不会重复建表,不能替代数据库迁移;上线后修改字段时还要设计 schema 版本演进。数据库对象应由仓储层统一管理,避免每个页面新建一个互不知情的连接。异常要在初始化调用处报告并阻止写入入口,不能把初始化失败解释成“暂无草稿”。## 三、让旧版本的 UPDATE 自己失败重点不是按钮颜色,而是更新条件。保存指令应要求:这条工单存在,且数据库当前 revision 正好等于编辑页携带的 expectedRevision。条件不满足时,数据库更新行数应该为零,页面显示冲突并重新读取,不再重试同一个覆盖动作。第二段代码解决旧快照抢写的问题。这里使用 RdbPredicates 将工单 ID 和预期版本组合成条件,用 RdbStore.update 返回的受影响行数作为结果。版本号由当前预期值加一,不以机器时钟作排序依据。这段逻辑属于业务仓储,不能把 CONFLICT 当成 HarmonyOS 内置错误码。arktsasync saveIfCurrent(ticketId: string, expectedRevision: number, body: string): Promise<'SAVED' | 'CONFLICT'> { if (!this.store) { throw new Error('RdbStore not ready'); } const where = new relationalStore.RdbPredicates('DRAFT'); where.equalTo('ticket_id', ticketId); where.equalTo('revision', expectedRevision); const count = await this.store.update({ body: body, revision: expectedRevision + 1 }, where); return count === 1 ? 'SAVED' : 'CONFLICT';}````count===1` 比“Promise 没有抛异常”多了一层业务含义:SQL 执行成功,但没有符合版本的行,也不是业务保存成功。若底层抛出了磁盘错误、权限错误或数据库繁忙,应走另一条失败路径并保留输入,不能全部归入冲突。还要防止页面重复连点;可以在 UI 层增加 `saving` 锁,但锁只是减少重复动作,不替代数据库条件。![](https://i-blog.csdnimg.cn/direct/92836fcdb052448f9a9cc7c05ed4483d.png)## 四、从读出的游标重建版本,而不是猜版本第三段代码解决页面再次进入时如何取得真正的 `revision`。`ResultSet` 是需要释放的查询资源,不能因为只读一行就省略 `close()`。这里用 `goToFirstRow` 检查是否存在工单,结果可能为 `undefined`,交给调用方决定新建流程或显示数据缺失。arktsinterface DraftSnapshot { ticketId: string; body: string; revision: number; }async readDraft(ticketId: string): Promise<DraftSnapshot | undefined> { if (!this.store) { throw new Error(‘RdbStore not ready’); } const where = new relationalStore.RdbPredicates(‘DRAFT’); where.equalTo(‘ticket_id’, ticketId); const rs = await this.store.query(where, [‘ticket_id’, ‘body’, ‘revision’]); try { if (!rs.goToFirstRow()) { return undefined; } return { ticketId: rs.getString(rs.getColumnIndex(‘ticket_id’)), body: rs.getString(rs.getColumnIndex(‘body’)), revision: rs.getLong(rs.getColumnIndex(‘revision’)) }; } finally { rs.close(); }}````finally 的位置很关键。即使转换字段时发生异常,结果集也需要及时关闭。这个读取函数没有包含云端同步、用户身份与跨设备权限;RelationalStore在本例中是本地仓储。若实际产品通过分布式机制合并草稿,必须另外定义网络冲突和授权规则,不能凭本地 SQL 条件就声称实现跨设备一致性。## 五、复核界面的两个提示应同时存在第四段代码展示页面如何消费条件保存结果。页面需要持有当前编辑内容与读出的版本;“保存成功”后本地版本加一,“冲突”则提示用户加载最新值,并保留未保存的输入以便人工比较。这里省略了界面布局装饰和仓储实例注入,聚焦事件顺序。```arkts@State private revision: number = 3;@State private body: string = '补齐复核依据';@State private saveState: string = 'EDITING';private saving: boolean = false;private async saveDraft(repo: DraftRepository): Promise<void> { if (this.saving) { return; } this.saving = true; try { const result = await repo.saveIfCurrent('RG-208', this.revision, this.body); if (result === 'SAVED') { this.revision += 1; this.saveState =SAVED_REV${this.revision}; } else { this.saveState = 'CONFLICT'; } } catch (err) { this.saveState = 'FAILED'; } finally { this.saving = false; }}```一个常见误区是发生冲突后立即把页面的 body替换成数据库新内容。这样虽然看起来恢复到最新版本,却会直接丢失用户刚才尚未提交的文字。更合适的设计是提示“本地编辑与最新版本不一致”,允许复制差异或选择保留本地草稿。保存完成后才更新版本,不能在发起 SQL 前乐观自增,否则异常后 UI 显示的版本与存储不一致。![](https://i-blog.csdnimg.cn/direct/93281daf5de641df8dd201d178be925e.png)## 六、怎样检查版本门禁,而不是检查绿色标签运行效果图固定展示DR-210、RG-208、版本 4、状态 SAVED_REV4、已保存 1、冲突阻断 1。示例时间 **11:08**、状态栏电量和手机界面都属于视觉设计元素。左侧为工单摘要,右侧为复核正文:这是把平行视界的双栏场景抽象成同屏交互,不是对系统平行视界路由 API 的编译验证。复现时先向测试库写入 RG-208 / revision=3的初值,再使用预期版本 3 保存一次,应得到SAVED,随后读到版本 4。接着故意用预期版本 2 写另一段旧内容,调用应该返回 CONFLICT。最后重新查询,检查 body 仍是新文本、revision 仍为 4。这样可以在没有多台设备的情况下先检查仓储不变量,然后再把 UI 切换、后台恢复、双窗口异步加载加入测试矩阵。建议按下列形式记录**预期**日志:[SplitDraft] task=DR-210 ticket=RG-208 savedRevision=4,以及 [SplitDraft] rejected expectedRevision=2 actualRevision=4。后一句的 actualRevision需要在冲突后重新查询才能打印,不应凭expectedRevision+1推测。任何性能耗时、并发成功率、数据库吞吐都不在本篇的证据范围内。## 七、还没有解决的事情条件更新挡住了单条记录的旧版本覆盖,却不负责自动合并两个不同人改的段落;它也不能替代应用被强杀后的输入持久化策略。若用户长时间离线,何时生成草稿记录、是否自动保存、冲突后如何恢复光标、是否加密敏感材料,需要另外定义。尤其在应用退出时,不应把尚未await 完成的写入当成成功,更不能以弹出 Toast 证明磁盘已经持久化。RelationalStore的update与查询 API 应在当前开发目标中核对,保留ResultSet.close()` 和异常分支。本文参照以下华为官方资料:- 关系型数据持久化:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-rdb-store- RdbStore API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-data-relationalstore-rdbstore- RdbPredicates API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-data-relationalstore-rdbpredicates本次交付是工程实现示例与预期验证方案,不是已编译通过的完整工程,也没有真实运行数据库写入、系统平行视界适配或上架审核经历。实际项目应将页面片段、仓储初始化、测试夹具及异常呈现完整装配,再在 DevEco Studio 和真实目标设备上逐项确认。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐