HarmonyOS 撤回处理与多窗口内容核对(连载06)【共创稿事节】
发布确认已经有了独立快照,但同一份座签在两个窗口中打开后,还会遇到另一个问题:左边停留在旧发布记录,右边已经完成新确认,左边再点撤回时,究竟撤回哪一份内容?如果撤回只表示把当前对象清空,旧窗口就可能误伤新发布。座签工程需要让每个写操作带上读取时的修订,并在统一入口核对。

撤回是一项带前提的状态变化
NameplateDesk继续沿用前篇ReleaseSnapshot和个人编辑模型。本篇新增发布记录,保存记录修订、当前快照和状态。记录修订覆盖发布与撤回两类动作,快照中的顺序修订仍只描述座位排列,两者职责不同。姓名变化而座位不变时,顺序修订可以保持,发布记录仍需推进。
窗口读取记录时保存seenRevision,写入时提交这个值。统一服务比较期望修订与当前修订,相同才执行,不同则要求刷新。比较与写入必须处于同一个同步动作中;若先比较、等待其他异步操作、再写入,就重新打开了竞争窗口。
本篇代码将协调规则放在独立的纯业务模块中。多个页面接入时应通过同一持有者调用这些函数,并在返回后更新各自的展示状态。跨进程或服务端存储接入时,存储侧也要提供原子的条件更新,页面自己的变量比较只能处理本地视图,不能替代存储事务。
发布记录复制快照并检查修订
新增model/ReleaseLedger.ets。copySnapshot复制行数据,发布与读取都避免泄露内部引用。这里的函数接受当前完整记录并返回新记录,共同持有者需要在调用后立即替换保存值。撤回后保留新的记录修订和已撤回状态,不退回到最初的零修订。
import { ReleaseRow, ReleaseSnapshot } from './ReleaseSnapshot';
export interface ReleaseLedger {
revision: number;
snapshot: ReleaseSnapshot | null;
status: string;
}
export function copySnapshot(source: ReleaseSnapshot): ReleaseSnapshot {
const copy: ReleaseSnapshot = {
orderRevision: source.orderRevision,
rows: source.rows.map((row: ReleaseRow): ReleaseRow => ({
id: row.id, seat: row.seat, name: row.name, role: row.role
}))
};
return copy;
}
export function createLedger(): ReleaseLedger {
const ledger: ReleaseLedger = { revision: 0, snapshot: null, status: '待发布' };
return ledger;
}
export function publishLedger(ledger: ReleaseLedger, expected: number,
snapshot: ReleaseSnapshot): ReleaseLedger {
if (expected !== ledger.revision) { throw new Error('发布记录已变化,请刷新'); }
const next: ReleaseLedger = {
revision: ledger.revision + 1, snapshot: copySnapshot(snapshot), status: '已发布'
};
return next;
}
export function withdrawLedger(ledger: ReleaseLedger, expected: number): ReleaseLedger {
if (expected !== ledger.revision) {
throw new Error('发布记录已变化,请刷新');
}
if (ledger.snapshot === null) { throw new Error('当前没有可撤回内容'); }
const next: ReleaseLedger = {
revision: ledger.revision + 1, snapshot: null, status: '已撤回'
};
return next;
}
同一个旧修订不能反复撤回:首次撤回成功会增加修订,重放旧操作随即被拒绝。若用户已刷新到撤回后的修订,再点撤回,则进入没有可撤回内容的分支。这两种失败含义不同,提示也应分别表达,便于判断是视图过期还是当前状态确实不允许操作。
撤回没有删除编辑资料。个人saved、draft和座位计划仍由原模块管理,用户可以继续整理内容并重新准备发布。把撤回误写成清空名单,会让发布生命周期与编辑生命周期耦合,后续恢复或核对都更加困难。
窗口视图保存自己的读取依据
新增model/WindowReview.ets。WindowReview保存已看到的修订与对应快照,refreshReview创建独立副本。reviewMessage只比较两项修订,明确告诉用户是否需要刷新。服务不自动把旧窗口的副本更新成新内容,避免正在阅读的确认对象无提示地改变。
import { ReleaseSnapshot } from './ReleaseSnapshot';
import { ReleaseLedger, copySnapshot } from './ReleaseLedger';
export interface WindowReview {
seenRevision: number;
snapshot: ReleaseSnapshot | null;
status: string;
}
export function refreshReview(ledger: ReleaseLedger): WindowReview {
const review: WindowReview = {
seenRevision: ledger.revision,
snapshot: ledger.snapshot === null ? null : copySnapshot(ledger.snapshot),
status: ledger.status
};
return review;
}
export function reviewMessage(review: WindowReview, ledger: ReleaseLedger): string {
return review.seenRevision === ledger.revision ? '内容已对齐' : '需要刷新';
}
窗口关闭时可以释放自己的视图对象,共同发布记录则由应用业务层继续持有。重新进入页面应重新读取,而不是沿用上一次离开时的修订。若产品需要保留阅读位置,可以保存位置,但数据身份和修订仍需重新核对。
是否自动刷新属于交互策略。只读列表可以在收到变化通知后更新,正在确认撤回的页面则适合先提示变化,再让用户看清新内容。无论采用哪种方式,通知只负责提醒有变化,实际写入依然携带seenRevision进行校验。
页面把冲突与刷新明确呈现
下面替换pages/Index.ets,构造一个可观察的冲突样本:页面先读取空记录的零修订,再由共同记录发布两人座签,当前记录变成一修订,而窗口仍保持零修订。刷新后才能撤回当前内容。个人资料、座序和快照捕获仍来自前五篇文件。
import { EditorState, createEditor } from '../model/RosterEditor';
import { SeatPlan, createSeatPlan, moveGuest } from '../model/SeatOrder';
import { ReleaseRow, captureSnapshot } from '../model/ReleaseSnapshot';
import { ReleaseLedger, createLedger, publishLedger,
withdrawLedger } from '../model/ReleaseLedger';
import { WindowReview, refreshReview, reviewMessage } from '../model/WindowReview';
@Entry
@Component
struct Index {
@State ledger: ReleaseLedger = createLedger();
@State review: WindowReview = refreshReview(this.ledger);
@State feedback: string = '';
aboutToAppear(): void {
if (this.ledger.revision === 0) {
const editor: EditorState = createEditor();
const seats: SeatPlan = moveGuest(createSeatPlan(editor), 'guest-csy', 1);
this.ledger = publishLedger(this.ledger, 0, captureSnapshot(editor, seats));
}
}
private withdraw(): void {
try {
this.ledger = withdrawLedger(this.ledger, this.review.seenRevision);
this.review = refreshReview(this.ledger);
this.feedback = '撤回已记录';
} catch (error) { this.feedback = String(error); }
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('会议座签').fontSize(26).fontWeight(FontWeight.Bold)
Text('发布记录核对').fontSize(20)
Text(`窗口修订 ${this.review.seenRevision}`).fontSize(18)
Text(`当前修订 ${this.ledger.revision}`).fontSize(18)
Text(reviewMessage(this.review, this.ledger)).fontColor('#9A5B00')
Text('当前发布').fontSize(20)
if (this.ledger.snapshot !== null) {
ForEach(this.ledger.snapshot.rows, (row: ReleaseRow) => {
Column({ space: 8 }) {
Text(`座位${row.seat} ${row.name}`).fontSize(22)
Text(row.role).fontSize(16)
}.padding(20).width('100%').backgroundColor('#FFFFFF')
}, (row: ReleaseRow) => row.id)
} else {
Text(this.ledger.status).fontSize(20)
}
Button('刷新记录').width('100%').onClick(() => {
this.review = refreshReview(this.ledger); this.feedback = '';
})
Button('撤回当前发布').width('100%').onClick(() => { this.withdraw(); })
Text(this.feedback).fontSize(16)
}.width('100%').padding(20)
}.width('100%').height('100%').backgroundColor('#F7F6EF')
}
}

页面卡片明确标为当前发布,修订标签则同时展示窗口依据和共同记录。真正接入两个窗口时,各窗口保存自己的WindowReview,共同持有者保存唯一ReleaseLedger;不能每个窗口都createLedger后各自推进,否则两个修订都可能是一样的数字,实际却没有共同数据源。
核对相同内容不能只看姓名
判断发布内容是否一致,需要按身份比较座位、姓名与职务,也要考虑人员新增和删除。两人姓名恰好相同并不意味着同一个身份,顺序修订相同也不能说明职务未变。窗口提示先使用记录修订快速识别过期,详细差异则应回到快照字段逐项说明。
字段核对时可以把行按ID建立索引,分别列出新增、删除和字段变化。座位变化与姓名变化应分开呈现,帮助用户判断需要重新安排座次还是仅更新称谓。对于当前两人样本,直接查看完整卡片足以定位;名单扩大后仍保持相同身份规则。

让失败分支成为可定位的信息
排查时保留操作携带的期望修订、当前修订和动作类型。旧修订被拒绝后,记录内容和修订都应保持原值;刷新后撤回成功,快照清空但修订增加。再次撤回不应继续增加修订。观察这三个动作,可以发现失败分支误推进计数或重放操作漏校验的问题。
再检查读取副本隔离:修改窗口保存的姓名,不应改变共同记录;修改发布时传入的快照,也不应改变记录中已经保存的内容。否则修订虽然没有变化,字段却能绕过服务发生变化,任何基于修订的冲突判断都会失去可靠前提。
实际接入异步持久化时,应在结果返回后核对操作所属的页面周期。旧页面已关闭或已切换记录,就不要把旧响应直接写回新视图。服务端返回的新修订应成为下一次读取依据,客户端不能自行推测远端已经推进到哪个值。
从排版走到发布生命周期
同步入口要有唯一持有者
把publishLedger和withdrawLedger放进一个共同仓库后,仓库方法应同步读取当前记录、调用函数并保存返回值。窗口只提交期望修订和动作参数,不自行决定新的全局修订。如果窗口把自己保存的旧ledger作为当前记录传回服务,校验比较的两边都会变旧,冲突保护就形同虚设。
同步规则适用于同一协调域内的串行调用。窗口使用不同工作线程或独立进程时,需要消息传递或共享存储层承接写操作,不能直接跨域共享普通对象。把协调点放在业务服务而非页面中,才能让不同展示入口使用一致规则,也方便后续替换存储实现。
记录修订还需要与业务身份一起使用。本例始终处理同一场会议,若扩展到多个会场,应先以会议身份选择记录,再比较该记录的修订。两个会议都存在修订一并不构成内容相同,窗口切换会议时也不能保留上一场的读取依据。
发布与撤回反馈遵守相同周期
发布成功后,新快照成为共同记录的一部分,其他窗口可标记为需要刷新;撤回成功后,共同记录变成空快照,其他窗口同样需要重新读取。不要只通知发布而遗漏撤回,否则展示窗口可能一直保留已经撤回的卡片,直到用户主动重新打开页面。
通知丢失时,写入前的修订核对仍能阻止旧操作,但只读界面还可能停留在旧内容。产品可在窗口重新获得焦点或用户主动刷新时补读记录。补读属于读取策略,不能为了让界面及时变化而取消写入条件,否则只是用一致外观掩盖竞争问题。
这套座签工程从姓名和版面约束开始,逐步区分了个人草稿、稳定身份、座位排列、发布快照和共同记录。各层只保存自己负责的数据,变化也各有明确入口。窗口宽度改变可以重算版面,换座位可以推进顺序修订,发布与撤回则推进记录修订。
当多个窗口同时参与时,可靠性来自共同数据源和条件更新,而非界面上是否显示了锁。保留清楚的读取依据、拒绝旧操作并提供刷新入口,才能让用户理解正在确认和撤回的是哪一份座签内容。
更多推荐

所有评论(0)