在神兽详情里继续阅读时,下一张卡片需要说明“推荐了谁”和“为什么推荐”。山海万灵把这两个问题收敛为图谱推荐契约:页面接收候选节点、分数和关系理由,再把候选神兽渲染成可打开的图鉴卡片。这样,推荐入口不会把接口字段、离线策略和展示文案混在同一个页面里。

推荐结果先固定为可读契约

客户端使用一个稳定模型承接不同数据来源。name 用于识别候选节点,graphReason 是面向读者的关联说明,score 保留排序依据;页面不需要知道候选来自 HTTP 还是本地数据。

export interface GraphRecommendationItem {
  nodeType: string
  nodeId: string
  name: string
  graphReason: string
  score: number
}

这组字段有两个直接收益:一是详情页只依赖统一模型,二是推荐理由成为数据的一部分,而不是组件临时拼出的猜测文案。候选节点缺少名称、关系理由或目标编号时,应在 Repository 映射阶段拒绝进入展示列表。

Repository 把接口结果转换为领域模型

在线数据路径以节点类型和节点编号查询推荐结果,随后把响应中的项目映射为客户端模型。调用方只调用 listGraphRecommendations,不拼接 URL,也不处理服务端响应外壳。

async listGraphRecommendations(nodeType: string, nodeId: string) {
  const response = await apiClient.getJson(
    '/graph/recommendations?nodeType=' + nodeType + '&nodeId=' + nodeId
  )
  return response.data.items.map(toGraphRecommendationItem)
}

映射函数同时完成最小字段筛选,避免不完整的服务端数据进入页面。这里不把接口对象原样透传;当节点编号或关系理由为空时,直接忽略该项,后续由空结果分支决定是否显示面板。

function toGraphRecommendationItem(item: ApiGraphRecommendation) {
  const nodeType = String(item.nodeType || '').trim()
  const nodeId = String(item.nodeId || '').trim()
  const name = String(item.name || '').trim()
  const graphReason = String(item.graphReason || '').trim()
  const score = Number(item.score)
  if (!nodeType || !nodeId || !name || !graphReason) {
    return undefined
  }
  return {
    nodeType: nodeType,
    nodeId: nodeId,
    name: name,
    graphReason: graphReason,
    score: Number.isFinite(score) ? score : 0
  }
}
层次 负责内容 不负责内容
Repository 请求、映射、字段校验 卡片布局与点击跳转
ViewModel 当前神兽上下文、候选列表状态 直接访问网络
ArkUI 组件 标题、候选卡片、关系理由 推测图谱关系

这种分层让详情页在替换服务端实现时保持稳定,也让 Mock 与真实接口具有相同的消费方式。

HTTP 失败时仍保留可用的探索入口

推荐不是详情页的唯一内容,网络异常不应让读者失去继续探索的入口。Fallback Repository 先尝试主数据源;请求失败后切换到本地实现,并将后续读取维持在可用路径上。

async listGraphRecommendations(nodeType: string, nodeId: string) {
  if (this.primaryReady) {
    try {
      return await this.primary.listGraphRecommendations(nodeType, nodeId)
    } catch (_) {
      this.primaryReady = false
    }
  }
  return this.fallback.listGraphRecommendations(nodeType, nodeId)
}

本地候选按照区域和展厅关系生成确定性排序:同展厅候选优先于普通候选,理由字段与排序依据同时返回。它适合离线浏览和回归验证;完整的个性化偏好、长期曝光去重和在线学习排序仍属于独立能力,不由这条本地路径替代。

本地规则把关系解释和排序放在同一处生成,保证每个候选都有可回读的理由。候选集合先排除当前神兽,再按同展厅、同区域和名称顺序确定结果,最终只保留有限数量的卡片,避免详情页被无关候选淹没。

function compareCandidate(left: Candidate, right: Candidate): number {
  if (left.sameHall !== right.sameHall) {
    return left.sameHall ? -1 : 1
  }
  if (left.sameRegion !== right.sameRegion) {
    return left.sameRegion ? -1 : 1
  }
  return left.name.localeCompare(right.name)
}

function explainCandidate(item: Candidate): string {
  if (item.sameHall) return '同展厅关联'
  if (item.sameRegion) return '同区域关联'
  return '补充图谱覆盖'
}

const visibleCandidates = candidates.filter(Boolean).slice(0, 4)

推荐组件只渲染已经返回的关系理由

推荐面板通过 nodeId 找回目标神兽,再将 graphReason 作为卡片的上下文说明。点击卡片后,页面使用目标编号打开对应的图鉴详情,避免把推荐结果复制成另一套静态内容。

ShanhaiBeastCard({
  beast: recommendationBeast(item),
  contextText: '关联:' + item.graphReason,
  onOpen: (beastId) => this.onOpen(beastId)
})

当前白泽详情页已显示“图谱推荐”区域;候选卡片“猰貐”展示了“关联:同展厅关联”。截图中的关系理由由推荐项字段提供,卡片仍保留神兽名称、分类和出没地等图鉴信息。

白泽详情页中的图谱推荐与同展厅关联理由

空结果和异常结果如何处理

推荐列表为空时,面板不渲染占位卡片;目标节点找不到对应图鉴时,组件不能把任意默认神兽当作推荐结果。服务端返回的关系理由需要与可查询的图谱边保持一致,不能用生成式文本替代候选召回。

场景 Repository 或组件处理 页面可观察结果
HTTP 请求成功且候选完整 映射并按分数输出候选 显示图谱推荐卡片与关联理由
HTTP 请求失败 切换到本地 Fallback 仍可看到确定性候选与理由
候选字段不完整 过滤无效项 不展示错误编号或空理由卡片
没有可用候选 返回空数组 推荐面板不出现占位内容

可按下面的顺序验收这条链路:选择一个已收录神兽,进入详情页,滚动到“图谱推荐”,确认至少一张候选卡片显示“关联:”理由;点击候选后,图鉴详情切换到对应节点。断网或服务暂不可用时,重复同一动作,仍应得到本地候选和可读理由。

为后续图谱召回预留替换点

当前排序只使用神兽、区域和展厅等已登记关系,分数的作用是让候选顺序稳定,并不宣称它代表用户偏好。更完整的召回服务接入后,可以继续返回相同的五个字段:服务端负责把边类型、边权重和过滤条件压缩为 graphReasonscore,客户端继续按 nodeId 取得图鉴实体并展示卡片。页面因此不需要跟随召回实现的变化反复调整。

例如,同展厅关系可以得到“同展厅关联”,同区域关系可以得到“同区域关联”;当同时满足多条关系时,服务端或本地实现应选出优先级最高且可解释的一条,而不是向卡片堆叠多句难以阅读的提示。理由文本应与实际候选来源一一对应:如果候选由展厅边得出,理由不能写成区域关联;如果候选来自人工精选,也应明确使用相应的关系类型。

在接口演进中,还需要保留三个约束。第一,nodeTypenodeId 组成目标节点的稳定身份,不能只依赖显示名称。第二,分数仅用于同一批候选排序,客户端不把它当作跨版本的业务指标。第三,空数组是合法结果,代表当前节点没有可展示候选;这比返回一个与当前节点无关的默认卡片更可靠。遵守这些约束后,详情页、馆长页或其他入口都可以共享同一推荐面板,同时保持关系说明的来源可追溯。

对读者而言,最直观的检查点是:卡片的标题能打开正确神兽,卡片下方的“关联”文字能解释本次候选来自哪条已登记关系;两者应同时变化,不能只更新其中一个。这样,推荐结果既可继续浏览,也能被快速复核。

小结

图谱推荐在这里是一条可替换的数据链路:契约负责表达候选和理由,Repository 负责切换 HTTP、Mock 与 Fallback,ArkUI 只展示已经返回的关系。先把理由固定在数据模型中,后续无论接入更完整的图谱召回还是个性化排序,详情页都能沿用同一套展示和跳转逻辑。

参考:HarmonyOS HTTP 数据请求官方指南

Logo

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

更多推荐