当前位置: 首页 > news >正文

羽球搭子 HarmonyOS 实战(18):个人统计聚合与排行榜数据模型

一、排行榜首先要回答“哪些数据算数”

一场活动可能同时存在未开始、计分中和已完成比赛。若统计服务消费所有场次,未开始的 0:0 会被当成平局,计分中的 18:16 会提前改变胜率。稳定规则是只聚合finishedAt > 0的比赛;参与者列表来自对局详情,即使某人尚未出场也应该显示 0 场,而不是从排行榜消失。

个人统计不只包含胜负。胜场适合决定第一排序键,胜率用于不同出场次数比较,净胜分反映比分优势,得失分用于解释净胜分,搭档组合则揭示双打协作表现。页面不重复计算这些指标,只消费统一的SessionStats。ArkUI 页面观察聚合结果的方式可参考ArkUI 状态管理概述。

二、先建立可验证的统计模型

PlayerStat应保存原始累计量和派生量:出场、胜、负、得分、失分是原始累计;净胜分和胜率可以在聚合末尾计算。把原始量保留下来,页面才能解释“为什么排在这里”,测试也能从逐场数据复算。

指标计算方式空样本规则用途
played参与的已完成场次数0样本量
wins/losses胜负累计0/0主排名与战绩
pointsFor所在队总得分0进攻表现
pointsAgainst对手总得分0防守表现
pointDiff得分 - 失分0同战绩比较
winRatewins / played × 1000%不同出勤量比较

SessionStats还应包含完成进度、排名后的选手列表、搭档组合、关键分洞察和摘要卡片。这样手机列表、平板侧栏和分享文本都使用同一结果,避免多个页面各算一遍。

三、从参与者初始化零值统计

聚合开始时按参与者创建零值对象,再遍历已完成比赛。不能只在比赛出现姓名时临时创建,否则尚未出场的人无法显示,姓名拼写差异也更难发现。比赛中的姓名若不在参与者池,应跳过并记录数据一致性问题。

function initPlayers(participants: string[]): PlayerStat[] { return participants.map((name: string): PlayerStat => ({ name, played: 0, wins: 0, losses: 0, pointsFor: 0, pointsAgainst: 0, pointDiff: 0, winRate: 0 })) } function finishedMatches(detail: SessionDetail): MatchItem[] { return detail.matches.filter((match: MatchItem) => match.finishedAt > 0) }

初始化后,任何页面都能得到固定顺序和完整名单。最后再做排行榜排序,不让“数据是否存在”和“排名如何”混在一起。

四、一次比赛要同时更新双方

对于双打比赛,A 队两名选手都增加一次出场;A 胜时两人各加一胜,B 队两人各加一负;得失分从各自视角写入。更新时生成新对象,避免修改同一引用导致 UI 观察不到变化。

function applyTeam( players: PlayerStat[], names: string[], pointsFor: number, pointsAgainst: number, won: boolean ): void { names.forEach((name: string) => { const index = players.findIndex((item: PlayerStat) => item.name === name) if (index < 0) return const old = players[index] players[index] = { ...old, played: old.played + 1, wins: old.wins + (won ? 1 : 0), losses: old.losses + (won ? 0 : 1), pointsFor: old.pointsFor + pointsFor, pointsAgainst: old.pointsAgainst + pointsAgainst } }) }

每场都对 A、B 两队各调用一次。这里不处理平局,因为羽毛球完赛数据不应是平局;若服务端传入平局,统计层应拒绝或把它标记为异常数据,不能默认为 B 队获胜。

五、派生指标在累计结束后统一计算

胜率应以真实胜场除以出场次数,保留一位小数。净胜分是累计得分减累计失分。所有场次累计后再计算,可以减少浮点舍入次数,也能确保页面拿到的各指标基于同一批数据。

function normalizeStats(players: PlayerStat[]): PlayerStat[] { return players.map((item: PlayerStat): PlayerStat => { const winRate = item.played === 0 ? 0 : Math.round(item.wins * 1000 / item.played) / 10 return { ...item, pointDiff: item.pointsFor - item.pointsAgainst, winRate } }) }

若旧模型曾用正负数混合编码胜负,迁移时应先恢复真实winslosses,不要把兼容公式继续扩散到新页面。统计公式越直接,越容易与服务端结果对账。

六、排行榜必须使用稳定排序键

只按胜场排序时,大量选手会同分;JavaScript 排序在不同数据来源下还可能暴露原始顺序差异。明确排序链为:胜场降序、胜率降序、净胜分降序、姓名升序。最后的姓名键不是为了业务公平,而是保证完全相同战绩仍有稳定展示顺序。

function rank(players: PlayerStat[]): PlayerStat[] { return players.slice().sort((left: PlayerStat, right: PlayerStat) => { if (right.wins !== left.wins) return right.wins - left.wins if (right.winRate !== left.winRate) return right.winRate - left.winRate if (right.pointDiff !== left.pointDiff) return right.pointDiff - left.pointDiff return left.name.localeCompare(right.name) }) }
选手战绩胜率净胜分排名理由
小羽3 胜 1 负75%+12胜场最高
阿林2 胜 0 负100%+8胜场少于小羽,排其后
子昂2 胜 1 负66.7%+15与阿林同胜场,胜率较低
小周2 胜 1 负66.7%+6同胜场胜率下比较净胜分

排序规则需要在界面附近说明,否则用户可能认为 100% 胜率理应第一。产品也可以改成先比较胜率,但必须在所有端和服务端保持一致。

七、双打还需要搭档维度

个人榜不能回答“和谁搭档表现最好”。组合键应先对两个人名排序,再用分隔符连接,使小羽|阿林阿林|小羽聚合为同一组合。组合累计出场、胜负和净胜分,最后使用与个人榜类似的稳定排序。

function pairKey(names: string[]): string { return names.slice().sort().join('|') } function applyPair(pairs: PairStat[], names: string[], won: boolean, diff: number): void { if (names.length < 2) return const key = pairKey(names) const found = pairs.find((item: PairStat) => item.key === key) if (found === undefined) { pairs.push({ key, names: names.slice().sort(), played: 1, wins: won ? 1 : 0, losses: won ? 0 : 1, pointDiff: diff, winRate: 0 }) return } found.played += 1 found.wins += won ? 1 : 0 found.losses += won ? 0 : 1 found.pointDiff += diff }

姓名可能重复时,生产模型应使用参与者 ID 作为组合键,姓名只做展示。仅靠姓名聚合会把同名球友合并,这也是账号关联后统计模型要升级的原因。

八、用可手算样本验证聚合

统计验收应选择能手工复算的小样本,而不是导入几十场后只看榜单“像不像”。准备 4 人、3 场比赛,确保每人至少出场一次,包含相同胜场但不同胜率、相同胜率但不同净胜分的情况。

1. 在完成 0 场时确认所有参与者显示 0 场、0%,摘要提示样本不足。 2. 完成一场 21:18,手算双方两人的得失分和净胜分。 3. 再完成两场,使两人同胜场但胜率不同,确认第二排序键生效。 4. 构造胜场、胜率都相同但净胜分不同的选手,确认第三排序键生效。 5. 检查同一搭档交换左右顺序后仍聚合为一个组合。 6. 修改完赛比分后重新计算,确认个人榜、搭档榜、趋势和分享文本同步变化。

九、总结

排行榜的可信度来自明确数据口径。只消费已完成比赛,以参与者池初始化零值统计,双方从各自视角累计得失分,在聚合末尾计算胜率与净胜分,再用稳定排序键生成个人榜;双打组合则使用顺序无关的组合键。页面只渲染SessionStats,分享和趋势也复用同一结果,才能避免同一场比赛在不同入口出现不同排名。

http://www.jsqmd.com/news/1244181/

相关文章:

  • IT66313:HDMI 2.1 Retimer
  • 数字身份的通行证:深入解析单点登录(SSO)的架构与艺术
  • 全中文作者命名Nature子刊!北京大学+天津大学+湖南大学Nature Communications:纳秒级超快存储器,中红外光电存储器的存算一体新突破
  • 2026济南空调维修哪家效率高?2家平台响应速度实测 - 简单到家
  • Hyprland 0.55 发布!带来 Lua 配置、自定义布局等重大更新!
  • CesiumJS终极指南:5大优势打造专业级WebGL地理可视化应用
  • 2026年7月最新泰格豪雅无锡万达广场(无锡惠山店)维修保养服务电话 - 亨得利钟表维修中心
  • 400电话底层技术拆解与企业落地实战:路由原理、呼叫架构、风控合规与对接方案
  • 科技查新怎么查?去哪查?线上平台与线下机构指南
  • “阅后即焚”的PrivateBin与乌龙事件
  • 解锁家庭音乐共享:用musikcube打造跨平台流媒体服务器
  • 如何快速掌握ARPL黑群晖安装:新手入门终极指南
  • IT66311:HDMI 2.0 Retimer
  • AgentScope 2.0:构建可观测、可理解、可信赖智能体的生产级架构实践
  • 命令行操作指南:从基础到高效开发实践
  • 2026年7月菲斯曼壁挂炉售后服务电话24小时全新专属热线升级公示最新公告 - 家电技术百科
  • 异形太阳能板定制:亲测有效的企业案例分享
  • springboot红色故事坊网站设计与实现
  • 你的AI编程工具API Key安全吗?从一次密钥泄露事件聊到BYOK机制
  • 智能体蜂群实验:新框架性能提升,不同模型组合成本差异巨大!
  • Android应用换肤的终极解决方案:AndroidChangeSkin深度解析与实战指南
  • AI辅助学术写作全流程优化指南
  • SCI睡眠模式与多处理器通信:实现嵌入式系统低功耗协同
  • 深度探索F3D:解密开源3D查看器的核心价值与实战应用
  • vim和nano的代替gedit
  • ST 表(Sparse Table)算法详解:原理、实现与应用
  • 2027皖芯展实现前沿技术与市场需求精准匹配,真正打通“研发—制造—封测—终端落地”的完整链路
  • 芝柏手表维修保养服务权威公示(2026年7月最新) - 亨得利官方服务中心
  • 高校院所如何提升技术转移的精准度与转化效率?
  • HTTPS与SSL关系