影展的站内提醒已经能够关联发布快照,观众也能把消息标记为已读。接下来要处理的场景并不复杂:用户从地铁进入影院,网络在两次刷新之间断开,回到首页后重新请求变更消息。假设订阅消息的连续序号已经收到108,服务端最新序号为110,客户端先获得110,却暂时没有取到109。直接把同步位置推进到110,下一次按“110之后”查询便再也无法补回109;如果坚持重新拉取整页,又会让107、108反复出现在列表里。

上一篇的已读水位解决的是用户确认过哪些提醒,本篇增加独立的接收水位。两个数字同处一个页面,却表示不同事实:消息收到以后可以暂存并校验连续性,只有用户主动确认阅读才能改变已读状态。把这两种位置混成同一个字段,既会漏提醒,也会在恢复网络后误把新消息显示为已读。

消息序列与补洞流程

从已读确认走向可靠增量接收

仍沿用前六篇的影展排片主线:场次拥有稳定的screeningId,每次正式发布形成releaseRevision,发布差异生成面向订阅者的站内提醒。与发布版本不同,seq是订阅消息流内单调递增的整数;一个发布版本可能产生多条提醒,一条提醒也只能分配一个稳定序号。后端可以按订阅账户维护消息流,但前端必须把它视为有序事实集合,而不是收到一页就自动确认全部已接收。

工程仍在entry/src/main/ets下组织。model/Notice.ets保存稳定消息身份;service/NoticeMerge.ets负责去重和补洞;service/NoticeFeed.ets负责取得增量数据;pages/SyncPage.ets只管理同步提示与列表状态。持久层保存接收水位、未连续的消息和已读信息,但不负责制定业务顺序。这样更换接口域名或调整页面布局时,序列处理规则保持不变。

先明确一个容易遗漏的前提:例子中的服务端应按同一订阅流提供连续序号,并允许按照某个序号向后分页查询。多个订阅用户各自拥有独立序列,不能把甲用户的108与乙用户的109拼成一条连续流。取消订阅、账户切换时,也应清空或切换当前流的本地存储命名空间,而不是沿用旧游标。

把序列和消息内容分开建模

对于消息去重,单靠电影名称与发布时间不够稳妥。一天可能两次调整同一场次,提醒标题也可能相同。更可靠的做法是同时保存唯一消息标识和流内序号,并规定同一序号的消息身份不可发生冲突。releaseRevision只用于展示跳转到哪个排期快照,不能替代接收序号。

// model/Notice.ets
export interface Notice {
  id: string;
  seq: number;
  screeningId: string;
  releaseRevision: number;
  title: string;
  detail: string;
}

export interface SyncSnapshot {
  streamId: string;
  receivedThrough: number;
  readThrough: number;
  messages: Notice[];
}

export function validNotice(n: Notice): boolean {
  return n.id.length > 0 && n.screeningId.length > 0 &&
    Number.isInteger(n.seq) && n.seq > 0 &&
    Number.isInteger(n.releaseRevision) && n.releaseRevision > 0;
}

这里的receivedThrough定义为“从初始位置开始已确认连续接收到的最后序号”,readThrough表示连续已读的最后序号。上一篇保留的乱序已读确认集合仍可继续使用,本篇不改写阅读策略。为了让两个位置互不干扰,同步操作只更新receivedThrough与消息缓存;阅读操作由独立的确认流程维护。

如果消息序号为107、108、110,数据列表可以保留110供后续处理,但连续接收位置只能停在108。界面需要明确区分“消息已经暂存”和“前面的缺口已经补齐”。用户在断网状态下查看缓存也不会改变同步算法。对于带有格式错误、序号为负数或者缺少业务ID的数据,不应把它们当作连续流的一部分。

合并算法先识别重复,再推进连续水位

合并过程的输入是当前快照和本次增量数组,输出是新快照以及下一个待补序号。算法不依赖网络请求完成的先后顺序:先用序号建立映射,再按序重新计算水位。若两个记录占用同一序号而id不同,应将它当作上游数据一致性问题,保留旧状态并给出清楚的错误提示,而不是随便选择一条覆盖。

// service/NoticeMerge.ets
import { Notice, SyncSnapshot, validNotice } from '../model/Notice';

export interface MergeResult {
  snapshot: SyncSnapshot;
  nextMissing: number;
}

export function mergeNotices(current: SyncSnapshot,
  incoming: Notice[]): MergeResult {
  const bySeq = new Map<number, Notice>();
  for (const item of current.messages) {
    bySeq.set(item.seq, item);
  }
  for (const item of incoming) {
    if (!validNotice(item)) {
      throw new Error('消息字段无效');
    }
    const previous = bySeq.get(item.seq);
    if (previous && previous.id !== item.id) {
      throw new Error(`序号${item.seq}对应不同消息`);
    }
    if (!previous) {
      bySeq.set(item.seq, item);
    }
  }
  let contiguous = current.receivedThrough;
  while (bySeq.has(contiguous + 1)) {
    contiguous += 1;
  }
  const merged = Array.from(bySeq.values())
    .sort((a: Notice, b: Notice) => a.seq - b.seq);
  return {
    snapshot: { ...current, receivedThrough: contiguous, messages: merged },
    nextMissing: contiguous + 1
  };
}

用receivedThrough=106作为起点,输入107、108、108、110,107和108可将连续水位推进到108;重复的108因标识相同只保留一次;110进入缓存,109成为下一处需要追补的位置。当109随后抵达,循环会连续跨过109、110,水位一次推进至110。这一设计使重试次数不影响最终消息集合,合并函数用相同输入重复计算也不会凭空增加消息。

消息缓存要有上限。移动端不宜永久保存所有历史消息,清理策略可以删除确认已读且超过保留期限的旧记录。不过要特别注意:合并算法需要知道历史水位,删除旧数据后也不能把接收位置归零。服务端若主动压缩历史,应该返回明确的重置版本和对应快照,客户端应进行一次受控重建,而不是猜测缺口已经不存在。

网络层按游标拉取,且保证资源释放

增量接口属于应用自己的业务服务。下面使用HarmonyOS Network Kit中的http.createHttp()与request()建立请求,实际服务地址由环境配置提供。接口约定响应为UTF-8 JSON,结构包含streamId、items和hasMore;序列补洞仍由上面的合并器负责,不把hasMore=false误当作“消息序号一定连续”。工程需要在module.json5声明ohos.permission.INTERNET,并根据服务证书、账户身份和超时要求配置请求。

// service/NoticeFeed.ets
import { http } from '@kit.NetworkKit';

export async function fetchNoticePage(endpoint: string,
  after: number): Promise<string> {
  const request = http.createHttp();
  try {
    const url = `${endpoint}?after=${after}`;
    const response = await request.request(url, {
      method: http.RequestMethod.GET,
      expectDataType: http.HttpDataType.STRING,
      connectTimeout: 8000,
      readTimeout: 8000
    });
    if (response.responseCode !== 200 ||
      typeof response.result !== 'string') {
      throw new Error('增量消息响应异常');
    }
    return response.result;
  } finally {
    request.destroy();
  }
}

页面不能把原始JSON直接转换为UI列表。业务层需要解析并逐项检查字段类型、流身份以及消息数量,发现账户或streamId改变时停止合并;遇到重复页则继续从当前连续水位重试,不能因本次有缓存项就推高水位。服务器必须限制单页最大条数,客户端也应对返回数组长度设定合理上限,避免损坏的数据造成内存持续增长。弱网下可采用有限重试和递增间隔,但每次请求应复用上一次连续位置,而不是数组最后一个最大序号。

请求流程还存在页面退出的问题。用户从订阅同步页跳回排片首页,旧请求可能稍后才返回。合并器的结果应先写入当前账户的数据仓库,再由活动页面订阅状态变化;或者利用请求代次检查,避免退出后向旧页面实例回填状态。跨多页面共享的数据不宜由某一个组件的@State充当唯一数据源。

ArkUI 页面展示接收水位与缺口

页面只把快照映射成可阅读的内容。List与ForEach负责稳定排列消息;键值选择id而非数组下标,减少增量插入造成的节点错位。下面的refresh()由现有仓库协调网络获取与合并,再一次性更换@State数组。错误提示与加载状态分离,发生超时后仍显示已有消息,但在顶部提示本次追更未完成。

// pages/SyncPage.ets 中的核心页面结构
import { Notice } from '../model/Notice';

@Entry
@Component
struct SyncPage {
  @State rows: Notice[] = [];
  @State receivedThrough: number = 106;
  @State nextMissing: number = 107;
  @State loading: boolean = false;
  @State hint: string = '可检查最新排期提醒';

  aboutToAppear(): void {
    // 从业务仓库恢复已保存的快照后,更新以上状态
  }

  build() {
    Column({ space: 12 }) {
      Text(`连续接收至 ${this.receivedThrough}`)
        .fontSize(20).fontWeight(FontWeight.Bold)
      Text(`下一待补序号:${this.nextMissing}`)
        .fontColor('#B86D16')
      Button(this.loading ? '正在同步' : '检查新提醒')
        .enabled(!this.loading)
      List({ space: 8 }) {
        ForEach(this.rows, (item: Notice) => {
          ListItem() {
            Column({ space: 6 }) {
              Text(`${item.seq} · ${item.title}`)
              Text(item.detail).fontSize(13)
            }.padding(12).width('100%')
          }
        }, (item: Notice) => item.id)
      }.layoutWeight(1)
      Text(this.hint).fontSize(12)
    }.padding(16).height('100%')
  }
}

开发环境中的同步模型与界面

接收水位、已读水位与待补消息

图中的106表示已读确认位置,108表示当前连续接收位置,110属于暂存消息;这些状态相互独立。真正需要核对的不是提示文案是否显示“已同步”,而是在补到109之后,消息顺序、接收位置和已读信息是否仍符合约定。

排错要追到状态来源

排查列表重复时,先打印同一账户对应的streamId、接收游标、输入消息数量与去重后的数量。如果收到同一序号的不同ID,检查服务端是否错误地重新分配序号;如果109始终缺失,检查增量查询参数与服务端分页边界是否存在开闭区间误差。最值得保留的诊断字段是cursorBefore、cursorAfter、nextMissing和duplicateCount,而不是整份用户消息正文。

还可以设计四种明确的工程校验输入:顺序到达、乱序到达、重复到达和流身份变化。每种输入都对应可判定的状态结果。特别是从108跳到110的情况,界面可以显示待补109,但不应发出“完整同步”提示;当109补齐,110便无需重新插入。网络层负责把增量取回来,数据层负责恢复连续性,页面负责解释当前状态,三个职责各自独立。

官方资料可参考创建列表(List)、HTTP请求配置常见问题与HarmonyOS文档中心。项目采用的系统接口主要是ArkUI列表构建和Network Kit HTTP请求,消息序号与补洞算法属于应用业务层;系统SDK与设备能力应按实际配置选择对应文档版本。

这一步让影展提醒从“拿到一页数据”变成“维护一条可恢复的消息流”。连续接收游标解决漏消息,唯一消息身份解决重复消息,阅读水位则继续表达用户行为。下一篇可以在此基础上处理旧版本消息清理和客户端订阅解绑,使整个观众提醒生命周期闭环。

Logo

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

更多推荐