【寻迹校园 HarmonyOS NEXT 实战 23】相反类型召回与稳定 Top 3:候选列表为什么先做门禁再排序
【寻迹校园 HarmonyOS NEXT 实战 23】相反类型召回与稳定 Top 3:候选列表为什么先做门禁再排序
这是“寻迹校园 HarmonyOS NEXT 实战”系列第 23 篇。本文沿着
getMatchBundle()的真实执行顺序,说明丢失记录为什么只召回拾得记录、候选状态如何门禁、Top 3 应该在何时截断,以及当前仅按分数排序时同分顺序还缺少什么工程保证。

上图为原创生成的流程插画,不是应用截图。左侧的丢失与拾得记录先经过类型、状态和自排除门禁,只有合法候选才能进入评分、排序与 Top 3。
一、召回和排序是两个不同阶段
很多列表实现直接把所有记录算分并排序,看起来只有一条链。业务上应明确拆成:
- 召回:决定哪些记录有资格参与;
- 排序:决定合法候选的相对位置;
- 截断:决定本次界面展示多少条;
- 解释:说明排序原因和冲突;
- 失效:权威记录变化后重新计算。
召回错误会把非法记录带入后续所有阶段;排序错误只影响合法候选的顺序。两者的风险和测试方法不同。
二、为什么必须召回相反发布类型
丢失记录的目标是寻找拾得记录,拾得记录的目标是寻找丢失记录。因此门禁是 report.reportType !== query.reportType。
若不做这一步,两条“丢失黑色双肩包”可能因为分类、地点和日期都一致而获得高分,但它们只能说明两个人都在找包,不能形成认领对象。
相反类型门禁属于领域规则,应放在 ReportService。页面临时隐藏同类型条目并不安全,因为小艺适配器、测试或其他入口仍可能调用同一候选数据。
三、为什么要排除查询记录自身
report.id !== query.id 看似多余,因为相反类型已经会排除自身。保留它仍有价值:
- 明确表达候选不能是查询对象本身;
- 防止未来类型兼容、历史脏数据或迁移错误破坏假设;
- 让测试和评审一眼看到自排除契约;
- 后续若扩展“相似记录合并”,不会意外复用错误逻辑。
冗余门禁只要语义清晰且成本极低,可以作为防御式约束;但不能用它掩盖数据层主键或类型错误。
四、状态门禁决定记录是否仍具备业务资格
当前候选只接受 OPEN 和 CLAIMING:
| 状态 | 是否召回 | 原因 |
|---|---|---|
OPEN |
是 | 正在公开展示和匹配 |
CLAIMING |
是 | 正在处理申请,仍可解释当前记录,但新动作受 Service 门禁 |
DRAFT |
否 | 尚未发布 |
RESOLVED |
否 | 已完成交接,不应产生新候选 |
WITHDRAWN |
否 | 发布者已停止公开与匹配 |
HIDDEN |
否 | 治理流程已隐藏 |
是否让 CLAIMING 继续出现在候选中属于产品策略。当前代码允许展示,但 ClaimService 会阻止对非 OPEN 记录再次提交认领,避免重复申请。
五、正确顺序是门禁、评分、排序、截断
当前实现顺序如下:
const candidates: MatchCandidate[] = reports
.filter((report: ItemReport) => report.id !== query.id && report.reportType !== query.reportType &&
(report.status === ReportStatus.OPEN || report.status === ReportStatus.CLAIMING))
.map((report: ItemReport) => this.scoreCandidate(query, report))
.sort((left: MatchCandidate, right: MatchCandidate) => right.score - left.score)
.slice(0, 3);
如果先 slice(0, 3) 再排序,结果取决于 Repository 原始顺序,高分候选可能永远进不了前三;如果先评分再过滤,非法记录会消耗计算并可能进入中间数据;如果页面再做一次截断,不同调用方会看到不同集合。
业务 Service 应返回完成门禁与排序的候选契约,页面只负责展示。
六、为什么当前选择 Top 3
比赛演示中,Top 3 能在手机屏幕上保持较低的认知负担,也能让用户逐条查看相似点与冲突。小艺适配器同样最多取前三条并压缩到 240 字,保证显式复制内容短、可读、可脱敏复核。
Top 3 不是算法正确性的证明,也不是所有场景的永久上限。若真实校园数据中多个相似物品集中出现,三条可能造成漏看;若只有低质量候选,展示三条又可能制造噪声。
正式产品可以增加“查看更多”,但第一屏和 AI 摘要仍应保持有限集合。
七、当前“稳定 Top 3”还缺一个显式二级键
代码只比较 right.score - left.score。当两个候选同分时,比较器返回 0,顺序会继承输入列表的相对顺序;但业务代码没有把 Repository 排序规则和同分行为写成明确契约。
因此当前可以说“按分数降序且同分通常继承上游顺序”,不能声称“跨数据源、跨版本绝对稳定”。若 Repository 从内存回退改为数据库查询,或者查询未固定 ORDER BY,同分前三可能变化。
一个更明确的比较器可以使用分数、事件时间、创建时间和 ID:
function compareCandidate(left: MatchCandidate, right: MatchCandidate): number {
if (left.score !== right.score) return right.score - left.score;
if (left.report.createdAt !== right.report.createdAt) {
return right.report.createdAt - left.report.createdAt;
}
return left.report.id.localeCompare(right.report.id);
}
这是建议方案,不是当前已合入代码。二级键的业务方向也需要确认:优先更新还是更早发布,不能只为“稳定”随便选择。

上图展示门禁后的候选先按相似分排队,再用明确二级键打破同分,最后截取前三。稳定性来自排序契约,不来自“这次运行看起来没变”。
八、稳定顺序为什么影响用户信任
用户刷新页面时,如果数据未变化但第一名和第二名互换,会怀疑算法随机。对小艺链路而言,Top 3 变化还会改变摘要内容,使同一查询得到不同辅助解释。
稳定顺序也有利于测试和问题复现:测试报告能准确记录候选 ID,开发者可以比较规则变更前后的差异,而不是被输入顺序噪声干扰。
但稳定不等于正确。二级键只能消除同分随机性,不能弥补错误权重或遗漏候选。
九、Top 3 截断之前不要丢掉解释数据
评分阶段应同时生成 score、reasons 和 conflicts,排序阶段只比较分数及稳定键,截断后仍保留完整解释。
如果为了性能先只计算分数,进入前三后才补理由,两个实现可能因规则重复而漂移;如果理由来自另一个服务,页面又会出现分数和解释不一致。
一次评分生成一份不可分割的候选结果,更容易测试和审查。
十、候选列表如何随状态变化失效
候选不是单独持久化的权威表,而是当前报告列表的派生结果。报告发生这些变化时,下一次调用会自然重新计算:
- 拾得记录被撤回或隐藏:不再通过状态门禁;
- 安全交接完成:变成
RESOLVED,退出候选; - 公开分类、地点或日期被编辑:重新评分;
- 新的对向记录发布:进入召回集合;
- 认领被拒绝或取消:状态恢复
OPEN。
当前页面通过数据变更回调和重新查询刷新。若未来引入候选缓存,必须为这些动作定义失效事件和版本号。
十一、空候选要区分三种原因
Top 3 为空可能来自:查询记录不存在、合法召回集合为空、Repository 或计算失败。
getMatchBundle() 对查询不存在返回失败;对合法候选为空返回成功的空集合;存储异常则应映射为错误。页面不能把三者统一写成“暂无数据”。
准确区分后,用户才能采取正确动作:返回重新选择报告、等待新信息、完善公开描述,或者重试读取。
十二、测试不只断言长度小于等于三
当前自动化已经验证:结果成功、查询 ID 正确、候选数量在 1~3、全部为相反类型、都有理由、分数非递增。
还需要增加:
- 同类型高分记录仍被排除;
DRAFT/RESOLVED/WITHDRAWN/HIDDEN全部被排除;- 查询自身不进入候选;
- 四条以上合法候选时截断发生在排序之后;
- 三条同分候选在固定二级键下顺序稳定;
- Repository 返回顺序改变时业务结果仍稳定;
- 候选状态变化后旧 Top 3 失效。
只有把 ID 顺序固定到断言里,才能证明“稳定 Top 3”。当前测试尚未覆盖显式同分二级键,因此本文把它列为改进项。
十三、大数据量时应把哪些门禁下推到数据库
当前实现先 list() 再在内存过滤,便于演示和测试。数据增长后,可以把相反类型、状态和自排除条件下推到 RelationalStore 查询,减少进入 ArkTS 层的记录数。
但数据库查询仍要有显式 ORDER BY,并保证与 Service 的评分契约一致。若评分规则不能完全下推,可以先用硬条件召回较小集合,再在 Service 中计算解释分。
任何优化都要通过相同测试向量验证结果一致,不能只比较响应时间。
十四、多人和多设备会带来新的竞态
当前项目是单机角色模拟。真实多人环境中,候选展示后可能立刻被他人认领、撤回或结案;用户点击时必须重新读取服务端权威状态。
Top 3 只是某个时间点的快照。提交认领时,ClaimService 或远端接口仍要检查目标是否 OPEN、是否已有待处理申请,并通过事务或版本号避免并发重复。
不能因为列表门禁正确,就推断后续写入没有竞态。
十五、可观测性应该记录规则版本而不是私密内容
排查排序问题时,可以记录查询 ID、候选 ID、规则版本、各维度命中标志和最终顺序,但不应打印手机号、证件号、私密核验答案或精确位置。
有了规则版本和候选 ID,开发者能重放测试;没有必要把用户私密描述复制进日志。可追踪性与最小化采集并不冲突。
十六、本文小结
稳定 Top 3 不是一句 sort().slice(0, 3) 就完成了。正确设计要先用相反类型、自排除和状态做召回门禁,再生成可解释分数,使用明确排序契约,最后截断,并在权威报告变化后重新计算。
“寻迹校园”当前已经完成门禁、降序排序和 Top 3,但同分候选尚未使用显式二级键。把这个缺口写清楚,比把一次演示顺序包装成永久稳定更符合工程实战。
系列导航:第 23 篇 / 共 50 篇。上一篇:《信息相似分不是所有权概率》;下一篇:《AI 不可用时的确定性降级》。
更多推荐



所有评论(0)