HarmonyOS 7 碰一碰精准分享投递回执与恢复闭环(连载04/04)【共创稿事节】
HarmonyOS 7 碰一碰精准分享投递回执与恢复闭环(连载04/04)【共创稿事节】
巡检照片准确落入平板报告之后,任务还没有真正结束。发送端需要知道“当前草稿是否已经完成”,接收端需要知道“文档里究竟写入了哪一条素材引用”。用户可能在确认按钮之后切换应用,也可能重新进入页面时只看到一个停留在“处理中”的状态。若完成信息只存在于按钮颜色或一次页面提示里,下一次进入时就容易重复投递,甚至把成功的记录当成失败重试。
这一节沿用前面三篇建立的 MaterialItem、ShareDraft、PlacementResult 和 DeliveryReceipt。前一节已经在写入前检查草稿有效期、素材修订号及目标文档修订号,并按 (draftId, documentId) 建立业务标识。本篇把关注点放到写入之后:接收端如何持久记录结果、发送端如何展示相同结果,以及在响应丢失、页面重建或记录状态不明时怎样恢复一致的用户体验。

图1 接收端内容提交、回执记录与发送端状态更新形成闭环
把“处理阶段”和“业务结果”分开
回执不应只有 success: boolean。请求被接收、执行正在进行、图片已写入、回执已保存,是不同的时间点。用户能看到的“已完成”必须以文档写入确认后的稳定记录为依据,不能在开始传输时就提前显示。对没有拿到最终结果的请求,接收端应返回“待核对”并查询对应的业务标识;仅凭调用超时无法判断图片是否已经写入。
应用内可以把每次投递定义成一条轻量工作记录。它保存草稿身份、目标文档、插入记录引用、阶段、最近一次更新时间。上一节的 DeliveryReceipt 用来描述对外反馈,本节额外定义 DeliveryJournalItem,专门服务恢复逻辑。这样页面的提示文字可以随语言或交互变化,而持久数据仍然使用稳定的状态枚举,不会因为修改文案导致历史记录无法识别。
// model/DeliveryJournalModels.ets
import { DeliveryReceipt } from './DeliveryModels';
export type JournalPhase = 'prepared' | 'writing' |
'confirmed' | 'needs_review' | 'cancelled';
export interface DeliveryJournalItem {
draftId: string;
documentId: string;
phase: JournalPhase;
insertRecordId: string;
updatedAt: number;
receipt?: DeliveryReceipt;
}
export class JournalIdentity {
static key(draftId: string, documentId: string): string {
return draftId + '|' + documentId;
}
}
选择上述字段有两个原因。一是发送端刷新界面时只需要按业务键取回结果,不需要反复读取图片的二进制内容。二是当图片已经进入文档而回执未到达时,接收端可以用 insertRecordId 查询现存图片引用,确认它与本次草稿对应,避免直接再次执行写入。对于一个草稿包含多张图片的情况,文档存储层可以保存一组插入记录;本文先按上一节的原子提交约束处理一个完整草稿。
持久检查点不能替代编辑器事务
上一节的 DeliveryGate 使用 Map 和 Set 管理重复检测与处理中状态。这对当前进程内的连续点击有效,但页面被回收后,集合会重新建立。应用可以用 ArkData 的用户首选项保存少量恢复记录,例如最近完成或需要核对的投递。不过,首选项不是跨设备共享数据库,也不具备文档图片写入与回执保存的跨存储原子事务。因此,真正阻止重复插入的责任依然属于接收端编辑器的数据层,最好让文档记录与唯一业务键处于同一事务边界。
在页面恢复方面,首选项负责“记住上次的工作状态”。保存 DeliveryJournalItem 时只写入草稿身份、文档标识、阶段和插入记录引用,不缓存图片数据、跨应用传输载荷或用户隐私字段。对少量记录,按键存取便于查询;若要管理大量历史记录、复杂筛选或多进程并发,应该转向适合该业务的数据管理方案,而不是把整个历史列表塞入一个首选项值。
// service/ReceiptJournalStore.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { DeliveryJournalItem, JournalIdentity }
from '../model/DeliveryJournalModels';
export class ReceiptJournalStore {
private store: preferences.Preferences;
constructor(context: common.UIAbilityContext) {
this.store = preferences.getPreferencesSync(
context, { name: 'delivery_receipts' });
}
async save(item: DeliveryJournalItem): Promise<void> {
const key: string = JournalIdentity.key(
item.draftId, item.documentId);
await this.store.put(key, JSON.stringify(item));
await this.store.flush();
}
async load(draftId: string, documentId: string):
Promise<DeliveryJournalItem | undefined> {
const raw = await this.store.get(
JournalIdentity.key(draftId, documentId), '');
if (typeof raw !== 'string' || raw.length === 0) {
return undefined;
}
return JSON.parse(raw) as DeliveryJournalItem;
}
}
flush() 是恢复链路容易遗漏的步骤。只修改内存中的首选项实例,并不能把“下次进入仍可读取”当成已经实现的结果。调用前还要确认容器已经具备有效的 UIAbilityContext,并为存储错误准备回退路径。如果本地首选项不可读,界面应进入“需要核对结果”的状态,再向接收端查询实际文档记录,避免直接把历史进度清零。

图2 ArkData 记录、回执协调器和界面状态的工程结构
恢复逻辑从目标文档查询开始
设想用户确认插入后,文档服务已经保存图片,而返回消息因为页面退出未抵达发送端。此时本地工作记录可能仍停留在 writing。恢复协调器应优先向接收端文档模块查询该业务键是否已存在有效插入记录:若存在,补写 confirmed 回执;若查不到但业务操作有明确失败结果,可提示重新选择;若目标暂不可达,则保持 needs_review 并允许用户稍后核对。不能把“查询暂时失败”等同于“确定没有插入”。
需要明确,下面的 DocumentReceiptPort 是应用自己的文档服务契约,不是系统精准分享接口。它返回的 insertRecordId 应由接收端创建并保存;同一个业务键重复查询应得到相同记录。只有当查询结果是确定的,协调器才能更新本地记录。与上一节的 PlacementCoordinator 不同,本节不处理坐标转换,恢复过程中也不复用过期触点坐标。真正需要重新投递时,用户必须重新确认目标编辑区域与内容落点。
// service/DeliveryRecovery.ets
import { DeliveryJournalItem } from '../model/DeliveryJournalModels';
import { ReceiptJournalStore } from './ReceiptJournalStore';
export interface DocumentReceiptPort {
findInsertRecord(draftId: string,
documentId: string): Promise<string | undefined>;
}
export class DeliveryRecovery {
constructor(private journal: ReceiptJournalStore,
private port: DocumentReceiptPort) {}
async reconcile(draftId: string,
documentId: string): Promise<DeliveryJournalItem | undefined> {
const item = await this.journal.load(draftId, documentId);
if (!item) { return undefined; }
if (item.phase === 'confirmed' || item.phase === 'cancelled') {
return item;
}
try {
const inserted: string | undefined =
await this.port.findInsertRecord(draftId, documentId);
if (inserted && inserted.length > 0) {
item.phase = 'confirmed';
item.insertRecordId = inserted;
item.updatedAt = Date.now();
await this.journal.save(item);
} else {
item.phase = 'needs_review';
item.updatedAt = Date.now();
await this.journal.save(item);
}
} catch (_) {
item.phase = 'needs_review';
}
return item;
}
}
这里对“查询不到插入记录”保持审慎:它可能意味着尚未写入,也可能是索引或同步延迟。为了降低误判,接收端应定义查询一致性规则。例如在事务提交之后,按唯一业务键读取必须能够返回刚刚写入的记录;若后端存在最终一致性窗口,应返回明确的“结果待确认”,由应用采用有界的重试间隔继续查询。重试必须设置次数或时间上限,避免页面持续占用资源和反复唤醒设备。
页面展示不应该隐藏不确定状态
发送端恢复后,界面可能遇到四种体验:已确认插入,可显示“打开目标文档”;正在核对,展示当前进度并禁用重复提交;目标变化,需要用户回到编辑区重新选点;草稿已过期,需要用户重新选取素材。对“需要核对”,使用明确且不制造焦虑的表述,比直接显示失败更准确。历史列表可以按草稿和接收文档聚合,每条记录显示时间、当前阶段以及下一步操作,用户不必记住某次轻触发生在什么窗口。
在 ArkUI 页面中,让 @State 承担当前可见的展示文本和按钮状态;从 DeliveryRecovery 返回的新记录赋值给页面,触发视图更新。避免由多个控件各自查询首选项,否则不同组件可能读到不同时间点的状态。尤其在折叠屏和平板等多窗口场景,恢复入口、分享详情页与历史列表都应指向同一业务协调器,减少相同事件被多个页面重复处理的机会。
// pages/DeliveryHistory.ets
import { DeliveryJournalItem } from '../model/DeliveryJournalModels';
@Component
export struct DeliveryHistory {
@Prop record: DeliveryJournalItem = {
draftId: '', documentId: '', phase: 'prepared',
insertRecordId: '', updatedAt: 0
};
private phaseText(): string {
switch (this.record.phase) {
case 'confirmed': return '已插入目标文档';
case 'writing': return '正在处理,请勿重复提交';
case 'needs_review': return '正在核对接收结果';
case 'cancelled': return '已取消当前投递';
default: return '等待确认目标';
}
}
build() {
Column({ space: 12 }) {
Text('精准分享 · 投递进度')
.fontSize(22).fontWeight(FontWeight.Bold)
Text(this.phaseText())
.fontSize(17).fontColor('#1769E0')
Text('目标文档:' + this.record.documentId)
.fontSize(14).fontColor('#667085')
if (this.record.phase === 'needs_review') {
Text('可稍后查看结果,再决定是否重新选择')
.fontSize(14).fontColor('#667085')
}
}
.padding(20).width('100%')
.backgroundColor('#F4F8FF').borderRadius(14)
}
}

图3 投递完成、待核对与重新选择三类状态的界面呈现
用四条路径检查闭环是否成立
工程检查应重点覆盖四条路径。正常路径从素材准备、目标定位到文档写入,最终能按业务键找到稳定插入记录;重复路径多次提交同一草稿,返回同一个完成结果而不增加图片数量;失败路径在写入之前发生版本变化,原草稿被拒绝并要求重新定位;恢复路径在写入之后丢失响应,页面重建后通过文档记录查询补全回执。这些用例的关键断言应该放在接收文档内容和记录存储上,而不是只检查某个提示是否短暂出现。
日志排查时,围绕 draftId、documentId、阶段转换和 insertRecordId 组织事件,保留必要的时间信息即可。素材 URI、图片本体、个人资料与精确位置不应随手写入普通日志。若需要跨设备定位问题,可以把匿名化的关联标识与业务事件类型对照,再查看发送端请求、接收端写入和回执恢复三段时间线;这样既能发现重试风暴,也能识别“写入成功但回执晚到”的正常情况。
四篇连载到这里形成了一条连续工程主线:发送端通过素材对象和草稿固定投递意图;接收端依据目标窗口与编辑区域计算落点;执行前控制重复、失效和版本冲突;执行后以稳定插入记录完成回执和恢复。把每个环节的职责边界写清楚,比给所有步骤共用一个“分享成功”状态更有价值,也为后续增加多素材、不同文档格式及协同审阅留下可扩展的位置。
相关开发文档:ArkData 用户首选项 API、ArkUI、ArkTS 简介。
更多推荐

所有评论(0)