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

羽球搭子 HarmonyOS 实战(17):比分撤销与边界校验

一、撤销不是把较大的一方减一分

比分从 8:7 变成 8:8 后发现误触,正确撤销结果应该回到 8:7;仅比较当前大小会把 A 队减成 7:8。要恢复最近一次动作,必须记录“哪支队伍获得了这一分”,而不是根据最终比分猜测。多场比赛同时计分时,历史还要按比赛标识隔离,否则在第二场点击撤销可能改动第一场。

一个轻量方案是Map<matchId, Team[]>:每次有效+1将队伍压栈,撤销时弹出栈顶并把对应比分减一。空栈、比赛不存在、比赛已结束都直接拒绝。ArkUI 的状态更新仍使用不可变数组替换,具体状态管理原则可参考ArkUI 状态管理概述。

二、先定义撤销的语义边界

用户通常把“撤销”理解为回退最近一次按钮加分,而不是恢复任意历史快照。直接录入 15:12、重置为 0:0、从云端覆盖比分都会建立新的基线,旧的逐分历史已经无法解释,应立即清空。比赛结束后也不继续撤销;若产品需要修改完赛比分,应走独立的“编辑结果”流程并记录审计事件。

操作是否写入动作栈是否清空动作栈是否允许撤销
有效 +1是,记录 A 或 B
在 0 分执行 -1不改变状态
直接录入比分从新基线重新记录
重置比分重置前历史作废
比赛结束
云端快照覆盖以服务端快照为基线

这种定义简单且可向用户解释。如果要支持多步重做,还需要双栈和更多动作类型,但普通球场快记没有必要一开始就引入完整命令系统。

三、只记录真正生效的加分

动作栈必须在比分确认变化后写入。比赛已结束、标识无效或变化量为零时不能记录;减分也不能伪装成可撤销的加分。记录函数同时接收旧比分和新比分,只有目标队伍确实增加才压栈。

private recordHistory( matchId: string, team: Team, delta: number, before: Score, after: Score ): void { if (delta <= 0) return const changed = team === 'A' ? after.a > before.a : after.b > before.b if (!changed) return const stack = this.scoreHistory.get(matchId) ?? [] stack.push(team) this.scoreHistory.set(matchId, stack) }

如果未来支持“一次加 2 分”,动作记录最好升级成{ team, delta, before },否则一次撤销只能减一。当前所有快捷按钮都以单分为单位,队伍栈已经足够,但要把这个前提写进模型约束。

四、撤销按栈顶动作回退

撤销先检查栈是否存在,再检查比赛仍处于playing。顺序很重要:不能先pop再发现比赛已结束,否则历史被无声丢弃。比分回退使用非负收敛,完成替换后才弹栈;如果 UI 更新过程中抛错,历史仍保留,可再次尝试。

undoLastScore(matchId: string): UndoResult { const stack = this.scoreHistory.get(matchId) if (stack === undefined || stack.length === 0) { return { ok: false, message: '暂无可撤销的得分' } } const index = this.matches.findIndex((item: ScoreMatch) => item.id === matchId) if (index < 0 || this.matches[index].status !== 'playing') { return { ok: false, message: '当前比赛不可修改' } } const team = stack[stack.length - 1] const current = this.matches[index] const a = team === 'A' ? Math.max(0, current.playerA.score - 1) : current.playerA.score const b = team === 'B' ? Math.max(0, current.playerB.score - 1) : current.playerB.score this.replaceScore(index, a, b) stack.pop() return { ok: true, message: `已撤销 ${team} 队上一分` } }

返回结构化结果比在领域函数里直接弹 Toast 更容易测试。页面根据ok决定反馈样式,领域层只描述发生了什么。

五、比分边界必须在所有入口一致

除加减按钮外,快捷比分、文本输入、语音指令和云端事件都可能改变比分。每个入口各写一套校验会逐渐分叉。可以把整数解析、范围收敛和状态检查封装成共同函数,任何来源先得到ScoreValidation,通过后再更新。

function validateScore(rawA: number, rawB: number, maxScore: number): ScoreValidation { if (!Number.isFinite(rawA) || !Number.isFinite(rawB)) { return { valid: false, reason: '比分必须是数字' } } if (!Number.isInteger(rawA) || !Number.isInteger(rawB)) { return { valid: false, reason: '比分必须是整数' } } if (rawA < 0 || rawB < 0) { return { valid: false, reason: '比分不能为负数' } } if (rawA > maxScore || rawB > maxScore) { return { valid: false, reason: `比分不能超过 ${maxScore}` } } return { valid: true, scoreA: rawA, scoreB: rawB } }
边界输入处理原因
8.5:7拒绝逐分计分只接受整数
NaN:10拒绝文本解析失败
51:20(普通制)拒绝或明确收敛超出产品保护上限
22:20(普通 21 分)进入结束态满足到点且领先 2 分
21:20(抢 21)进入结束态到点且不平分

六、重置与直接录入要切断旧历史

重置比分后若保留[A, B, A],下一次撤销会试图从 0:0 减分;直接录入 18:16 后保留旧栈,则撤销得到的“上一分”未必对应 18:16 的真实形成过程。两种操作都应先确认比赛可修改,再替换比分并删除该场历史。

resetScore(matchId: string): boolean { const match = this.findPlayingMatch(matchId) if (match === undefined) return false if (match.playerA.score === 0 && match.playerB.score === 0) return false this.updateMatch(matchId, this.copyWithScore(match, 0, 0)) this.scoreHistory.delete(matchId) return true } applyDirectScore(matchId: string, a: number, b: number): boolean { const checked = validateScore(a, b, this.maxScoreOf(matchId)) if (!checked.valid) return false this.updateScoreFromBaseline(matchId, checked.scoreA!, checked.scoreB!) this.scoreHistory.delete(matchId) return true }

若需要撤销“重置”本身,应把重置建模成完整快照命令,而不是继续混用队伍栈。两种撤销语义不要同时隐藏在同一个按钮里。

七、自动结束后的纠错要走独立流程

普通 21 分制在 22:20 自动结束,系统会清空动作栈并保存结果。如果用户此时发现最后一分误触,直接撤销会让云端和统计页不知道比赛已从完成退回进行中。更安全的交互是进入“修正结果”弹窗,展示原比分和新比分,提交后产生score.updated事件,并重新计算胜方、结束时间和统计。

interface ScoreCorrection { matchId: string previousScoreA: number previousScoreB: number scoreA: number scoreB: number reason: string } function createCorrection(match: MatchItem, a: number, b: number): ScoreCorrection { return { matchId: match.id, previousScoreA: match.scoreA, previousScoreB: match.scoreB, scoreA: a, scoreB: b, reason: 'manual_correction' } }

这条路径比“让结束态重新可点击”多一步,但能保留审计信息,也便于云端按版本处理冲突。

八、用动作序列而不是单点比分验收

撤销测试应输入完整动作序列。只检查某个最终比分无法证明栈顺序正确,也无法覆盖多场隔离和基线切换。

1. A、B、B 依次得分,确认比分 1:2,连续撤销得到 1:1、1:0、0:0。 2. 在空栈继续撤销,确认仅提示且比分不变化。 3. 同时创建两场比赛,在两场分别加分,确认撤销只影响目标场次。 4. 形成 6:4 后直接录入 15:12,再撤销,确认提示空栈而不是回到 14:12。 5. 重置后再次加分,确认新动作栈从空开始。 6. 比赛结束后点击撤销,确认被拒绝;通过结果修正流程更新时产生独立记录。

九、总结

比分撤销的可靠性来自清晰语义:它回退最近一次真实加分,不猜测较大比分,不跨比赛共享历史,不越过直接录入和重置形成的新基线,也不修改已经结束的比赛。按场次隔离的动作栈配合统一边界校验,足以覆盖球场快记的大多数纠错场景;需要修改完赛结果时,再使用带审计信息的独立流程。

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

相关文章:

  • 终极解密利器AES-Killer:网络安全测试中的AES加密流量实时解密指南
  • 【超详细】二分查找(折半查找)核心知识点全解析(含多场景代码实现)
  • 2026年7月最新萧邦大连来福士维修保养服务电话 - 萧邦中国官方服务中心
  • 5个关键问题:GoFrame如何帮助企业级应用提升3倍开发效率?
  • DPPO环境扩展指南:如何将新机器人仿真环境接入训练框架
  • 2026年工商业储能系统选型指南:六款主流一体机深度对比 - 科技焦点
  • 树莓派部署BirdNET-Go常见问题解答:从硬件要求到性能优化
  • [JCMSuite] JCMSuite应用:等离子波导
  • json-swift高级技巧:函数式API带你玩转JSON数据处理
  • 2026记者采访整理录音,软件测评哪个好更适合新人使用
  • IT66318:HDMI 2.0 Retimer
  • 第四篇:《函数、指针与结构体:组织代码的核心工具》
  • Nativ:100%开源,让你在Mac本地运行AI模型,无需账户订阅!
  • 做公司PPT最烦的不是写内容,是套模板
  • B3843 [GESP202306 三级] 密码合规 题解
  • LLM输出稳定性差3倍?——从token级置信度、逻辑连贯性到领域适配度的8项硬指标深度拆解
  • 2026年经期记录小程序哪个好用?日常记录与周期预测实操经验 - 软件测评小帮手
  • 大数据hadoop的高校照明智慧监测预警系统
  • 2026 餐饮收银系统横向科普:二维火与客如云适配场景、能力差异解析
  • 2026 年文县专业的景区园林花岗岩石材订做厂家哪家专业,别让你的景区毁在石材选错上-天翔石业 - 行业推荐官[官方】--
  • AI全栈开发指南 2026 :从想法到代码只需几分钟:揭秘高效产品原型设计的AI工具栈
  • Codex工具生态与部署实践全解析
  • 崩坏星穹铁道自动化助手终极指南:如何让三月七小助手每天为你节省2小时游戏时间
  • 知网/万方/PubMed三库查重结果竟相差41.2%?揭秘AI搜索底层语义锚点偏移机制(含TensorFlow调试日志)
  • 性能/安全/异常场景提示词
  • Windows系统文件dwmghost.dll丢失找不到问题解决
  • 羽球搭子 HarmonyOS 实战(18):个人统计聚合与排行榜数据模型
  • IT66313:HDMI 2.1 Retimer
  • 数字身份的通行证:深入解析单点登录(SSO)的架构与艺术
  • 全中文作者命名Nature子刊!北京大学+天津大学+湖南大学Nature Communications:纳秒级超快存储器,中红外光电存储器的存算一体新突破