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

旧窗口与当前发布修订核对

撤回是一项带前提的状态变化

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作为当前记录传回服务,校验比较的两边都会变旧,冲突保护就形同虚设。

同步规则适用于同一协调域内的串行调用。窗口使用不同工作线程或独立进程时,需要消息传递或共享存储层承接写操作,不能直接跨域共享普通对象。把协调点放在业务服务而非页面中,才能让不同展示入口使用一致规则,也方便后续替换存储实现。

记录修订还需要与业务身份一起使用。本例始终处理同一场会议,若扩展到多个会场,应先以会议身份选择记录,再比较该记录的修订。两个会议都存在修订一并不构成内容相同,窗口切换会议时也不能保留上一场的读取依据。

发布与撤回反馈遵守相同周期

发布成功后,新快照成为共同记录的一部分,其他窗口可标记为需要刷新;撤回成功后,共同记录变成空快照,其他窗口同样需要重新读取。不要只通知发布而遗漏撤回,否则展示窗口可能一直保留已经撤回的卡片,直到用户主动重新打开页面。

通知丢失时,写入前的修订核对仍能阻止旧操作,但只读界面还可能停留在旧内容。产品可在窗口重新获得焦点或用户主动刷新时补读记录。补读属于读取策略,不能为了让界面及时变化而取消写入条件,否则只是用一致外观掩盖竞争问题。

这套座签工程从姓名和版面约束开始,逐步区分了个人草稿、稳定身份、座位排列、发布快照和共同记录。各层只保存自己负责的数据,变化也各有明确入口。窗口宽度改变可以重算版面,换座位可以推进顺序修订,发布与撤回则推进记录修订。

当多个窗口同时参与时,可靠性来自共同数据源和条件更新,而非界面上是否显示了锁。保留清楚的读取依据、拒绝旧操作并提供刷新入口,才能让用户理解正在确认和撤回的是哪一份座签内容。

相关资料:状态管理术语、条件渲染。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐