ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

灯光模拟HarmonyOS应用实战-62-保证近光为何还要再点一次-普通灯光与实操超时分支对齐

灯光模拟HarmonyOS应用实战-62-保证近光为何还要再点一次-普通灯光与实操超时分支对齐 灯光模拟HarmonyOS应用实战-62-保证近光为何还要再点一次-普通灯光与实操超时分支对齐在灯光模拟训练中“执行一次动作”和“维持某个状态”看起来很接近落到判题代码里却可能产生完全不同的结果。这篇文章面向维护 HarmonyOS/ArkTS 训练类状态机的开发者分析依据是本地The_kemusan工程中QuestionBank.ets与Index.ets的静态分支目标是把判题差异压缩成一个可审计的最小改动。当前题库中的p_meet指令写的是“夜间与机动车会车保证近光灯状态”。如果上一条指令结束后灯光本来就处于近光状态学员看到这句话时很自然地会认为当前状态已经符合要求不需要再点击一次“近光灯”。但从源码分支看同样是ACTION_LOW LIGHT_LOW 等待超时普通灯光模式和实操模式会走向不同结果普通灯光模式的handleTimeout会在超时点再次查看灯态若目标动作是ACTION_LOW当前灯态也已经是LIGHT_LOW则按正确结果收口。实操模式的handlePracticalTimeout只要当前指令仍未完成就按失败结果收口没有为p_meet保留已经满足近光状态的分支。这篇文章只处理这一个差异保留普通模式现有行为把p_meet的实操超时结果与之对齐并用差异回归锁住边界。文中的修改代码都是建议方案尚未写入项目源码。读完本文你会得到四个可以直接用于代码评审和回归测试的结果用同一条时间序列解释普通模式与实操模式为何出现不同判定。把“已有近光能否满足指令”收敛为一个没有页面副作用的纯判断。用差异矩阵锁定唯一允许变化的p_meet LIGHT_LOW业务格子。分开记录源码事实、建议实现、构建结果与设备操作避免把静态分析写成已经落地。一、先把问题压缩成一个可重复的时间序列这个问题不是“近光按钮能不能点击”也不是“灯态能不能切换”。真正需要比较的是两个训练入口在相同输入条件下超时回调给出了不同结论。可以把场景压缩为下面五步时间点普通灯光模式实操模式T0上一条操作结束上一条操作结束T1当前灯态为LIGHT_LOW当前灯态为LIGHT_LOWT2新指令期望ACTION_LOW新指令为p_meet动作字段也是ACTION_LOWT3学员没有再次点击近光灯学员没有再次点击近光灯T4handleTimeout查看现有灯态并可判正确handlePracticalTimeout直接进入失败收口这组输入的核心不是“学员什么都没有做”而是“指令出现时目标状态已经成立”。如果一条指令强调“保证近光灯状态”判题时就必须回答一个明确的问题这条指令要求产生一次新的近光动作还是要求超时前保持近光状态从当前普通模式的超时逻辑看项目已经存在“现有状态可以满足ACTION_LOW”这一行为。差异产生在实操超时分支没有复用它。为了避免讨论被页面样式、按钮动画、语音播放和历史记录分散本文只观察三个输入和一个输出interface TimeoutObservation { expectedAction: string currentLightState: string commandId: string passed: boolean }这段结构不是要加入业务模型而是帮助我们限定分析范围。expectedAction、currentLightState和commandId足以描述本次分支差异passed是需要对齐的结果。页面布局、计时器展示和语音内容都不参与这个最小判断。二、从 PracticalCommand 与 p_meet 确认源码事实实操指令来自QuestionBank.ets。当前PracticalCommand主要携带编号、文本、动作和分类p_meet使用ACTION_LOW表示近光要求。下面是只保留本文相关字段的等价摘录字段含义与当前源码一致排版为了讲解做了收缩并非逐字复制源码export interface PracticalCommand { id: string text: string action: string category: string } export const PRACTICAL_COMMANDS: PracticalCommand[] [ { id: p_meet, text: 夜间与机动车会车保证近光灯状态, action: ACTION_LOW, category: 会车 } ]这里有两个可以直接从源码确认的事实。第一p_meet的文案包含“保证近光灯状态”。它描述的是会车场景应当处于什么灯态并没有在文案中要求“必须重新点击近光灯按钮”。第二它的数据模型仍然把这条要求放在action字段里值为ACTION_LOW。因此判题代码如果只比较动作事件就会把“需要近光”解释成“需要再发生一次近光点击”如果在超时点同时读取灯态则可能把它解释成“当前近光已经满足”。源码事实与本文建议需要分开看内容当前可以确认的事实本文不直接断言的内容指令数据p_meet的action为ACTION_LOW产品设计者最初是否有意要求重复点击指令文案文案强调“保证近光灯状态”所有含ACTION_LOW的指令都应接受已有状态普通超时ACTION_LOW LIGHT_LOW存在正确分支该分支是否覆盖了全部业务场景实操超时当前没有对应的状态满足分支真机上已经有人因此失败本文方案只建议让p_meet复用现有状态语义尚未修改源码也没有执行运行验证最后一列很重要。源码结构可以说明“某个输入会进入哪个分支”却不能替代真实设备操作记录。本文没有把静态阅读推断写成已经发生的用户故障。三、两条超时路径的差异究竟在哪里普通灯光模式的handleTimeout并非一律把超时判为失败。它先确认回调仍对应当前训练再为近光状态保留一个补偿判断。以下代码只表达决定性条件是结构等价的收缩版不是对源码逐字复制private handleTimeout(expectedAction: string): void { if (!this.isExamActive) { return } if (expectedAction ACTION_LOW this.currentLightState LIGHT_LOW) { // 普通模式目标为近光而且超时点已经处于近光。 this.completeCurrentCommand(true) return } this.completeCurrentCommand(false) }真正决定本文结论的是第二个if即使等待期间没有收到新的近光点击只要超时点满足ACTION_LOW LIGHT_LOW普通模式仍可按正确结果结束当前指令。实操模式的handlePracticalTimeout也会先处理活动状态和当前指令等保护条件但在这些保护条件之后没有与普通模式对应的灯态判断。它的关键结构可以压缩为下面的等价伪代码同样不是逐字源码private handlePracticalTimeout(commandId: string): void { if (!this.isPracticalExamActive) { return } if (this.currentPracticalCommand?.id ! commandId) { return } // 实操模式当前指令仍在等待时直接按失败收口。 this.completeCurrentPracticalCommand(false) }因此差异不在ACTION_LOW常量也不在LIGHT_LOW的表示方式而在两个超时回调的最后一段普通分支先问“目标状态是否已经满足”再决定失败。实操分支保护条件通过后直接失败。p_meet恰好把状态语义写在文案里却进入了没有状态补偿的实操分支。还要注意超时函数通常不仅负责返回布尔值。它还可能清理定时器、更新提示语、增加得分、写入记录或切换下一条命令。因此修复时不宜复制整段普通模式代码到实操模式。复制越多两个入口以后越容易再次分叉。更合适的边界是只抽取“超时点是否已满足近光”这一小块纯判断原有收口流程仍由各自入口负责。四、先用特征测试固定当前分支差异在改变结果之前先写一个能描述现状的特征测试。它的作用不是证明哪个结果更合理而是让维护者清楚看到当前两个入口对同一组关键输入确实给出了不同输出。如果项目已经有可调用的超时测试入口可以直接驱动页面状态如果页面状态不方便构造也可以先通过很薄的适配层暴露结果。下面用runOrdinaryTimeout和runPracticalTimeout表示这两个入口它们是测试设计示例并非项目现有函数describe(当前超时分支特征, () { it(普通模式在 ACTION_LOW 与 LIGHT_LOW 同时成立时通过, () { const result runOrdinaryTimeout({ expectedAction: ACTION_LOW, currentLightState: LIGHT_LOW }) expect(result.passed).assertTrue() }) it(实操 p_meet 在相同灯态下仍然失败, () { const result runPracticalTimeout({ commandId: p_meet, expectedAction: ACTION_LOW, currentLightState: LIGHT_LOW }) expect(result.passed).assertFalse() }) })这两条测试都应当描述修改前的行为。第一条防止后续修复误伤普通模式第二条把待对齐的差异显式记录下来。如果直接测试页面方法计时器会让用例变慢也容易受到异步顺序影响。更稳妥的做法是把“定时器何时触发”和“触发后怎样判定”分开。测试只调用判定入口不需要真的等待若干秒。特征测试还应记录终态提交次数。一个超时回调和一个点击事件可能在相邻时间片内到达如果两个路径都能结束当前指令就需要防止重复加分或重复写入历史。it(点击收口后到达的超时回调不会再次提交结果, () { const session createPracticalSession(p_meet, LIGHT_LOW) session.handleAction(ACTION_LOW) session.handleTimeout(p_meet) expect(session.resultCommitCount).assertEqual(1) })这条用例与“已有近光是否通过”是两个维度。前者处理并发收口后者处理业务结果。不要因为加入灯态判断就删掉原有的活动状态、当前命令编号或已完成标记。五、抽取一个只负责超时近光判定的最小函数这次不需要重新设计整套灯光状态机。普通模式已经给出需要保留的行为只需把那条条件变成可复用的判定器并为调用方显式传入是否允许已有近光状态满足当前命令。可以先定义两个很小的结果类型enum TimeoutVerdictCode { SATISFIED_BY_LOW_STATE SATISFIED_BY_LOW_STATE, NOT_SATISFIED NOT_SATISFIED } interface TimeoutVerdict { passed: boolean code: TimeoutVerdictCode } function evaluateLowStateAtTimeout( expectedAction: string, currentLightState: string, acceptExistingLow: boolean ): TimeoutVerdict { const lowStateSatisfied acceptExistingLow expectedAction ACTION_LOW currentLightState LIGHT_LOW if (lowStateSatisfied) { return { passed: true, code: TimeoutVerdictCode.SATISFIED_BY_LOW_STATE } } return { passed: false, code: TimeoutVerdictCode.NOT_SATISFIED } }这个函数刻意保持狭窄它只处理超时点的近光满足条件。它不启动或取消计时器。它不修改页面状态。它不加分也不写历史记录。它不决定所有ACTION_LOW指令是否都允许沿用已有灯态。它通过acceptExistingLow把命令策略留给调用方。为什么不直接写成“只要ACTION_LOW LIGHT_LOW就通过”因为这次可以明确讨论的是普通模式的既有行为和p_meet的文案。其他实操指令即使也使用ACTION_LOW其语义仍需逐条确认。把开关作为参数可以将影响限制在已分析的入口。为什么返回TimeoutVerdict而不是单个boolean因为业务结果写入页面时通常还需要区分“通过一次动作完成”和“在超时点由已有状态满足”。一个稳定的原因码有助于测试和排查又不会让判定器直接依赖提示文案。判定关系可以写成一个很小的真值表acceptExistingLowexpectedActioncurrentLightState结果trueACTION_LOWLIGHT_LOW通过trueACTION_LOW非LIGHT_LOW失败true非ACTION_LOWLIGHT_LOW失败falseACTION_LOWLIGHT_LOW失败第四行让调用方可以继续维持其他实操命令的原行为避免一次局部修复扩大成未经确认的业务改写。六、普通灯光模式只替换条件不改变收口顺序普通灯光模式已经接受ACTION_LOW LIGHT_LOW。接入共享函数时目标是重构判断位置不能改变外部行为。建议结构如下这仍是尚未写入项目的接入示例private handleTimeout(expectedAction: string): void { if (!this.isExamActive) { return } const verdict evaluateLowStateAtTimeout( expectedAction, this.currentLightState, true ) if (verdict.passed) { this.completeCurrentCommand(true, verdict.code) return } this.completeCurrentCommand( false, TimeoutVerdictCode.NOT_SATISFIED ) }这里的第三个参数固定为true因为它对应普通模式已有的近光超时行为。接入前后以下行为都应该保持不变目标是近光超时点已经为近光继续通过。目标是近光超时点不是近光继续失败。目标不是近光即使当前恰好为近光继续失败。考试已经结束旧回调直接返回。当前命令已经被点击路径收口旧回调不能重复提交。实际项目中的收口函数名、提示文本和分数更新方式应沿用现有实现。上面的completeCurrentCommand只是表示“原有普通模式收口逻辑”不是对当前函数名的逐字摘录也不能为了套用示例而新增一套并行状态。普通模式接入共享函数还有一个作用它让回归测试能直接证明抽取前后输出一致。如果只在实操函数里临时补一个if虽然也可能解决p_meet但普通模式仍保留另一份条件表达式后续修改其中一处时差异可能再次出现。可以用差异用例验证抽取没有改变普通模式const ordinaryCases: ArrayTimeoutObservation [ { expectedAction: ACTION_LOW, currentLightState: LIGHT_LOW, commandId: ordinary_low, passed: true }, { expectedAction: ACTION_LOW, currentLightState: LIGHT_HIGH, commandId: ordinary_low, passed: false }, { expectedAction: ACTION_HIGH, currentLightState: LIGHT_LOW, commandId: ordinary_high, passed: false } ] ordinaryCases.forEach((item: TimeoutObservation) { const verdict evaluateLowStateAtTimeout( item.expectedAction, item.currentLightState, true ) expect(verdict.passed).assertEqual(item.passed) })这组用例覆盖了条件中的三个变量方向动作相同但状态不同、状态相同但动作不同以及两者同时满足。只测成功用例会漏掉“其他动作被近光状态误放行”的风险。七、实操模式只为 p_meet 打开已有近光分支实操入口需要更谨慎。现有静态证据不足以支持把所有实操ACTION_LOW指令都改成状态型指令因此先用命令编号明确限定p_meet。可以增加一个简单策略函数function acceptsExistingLowAtTimeout( command: PracticalCommand ): boolean { return command.id p_meet command.action ACTION_LOW }同时比较id和action有两个好处。一是表达业务对象确实是p_meet而不是任何碰巧使用同一动作常量的命令。二是当题库以后误改了p_meet的动作字段时函数不会悄悄继续放行而会让回归用例暴露数据与策略不一致。实操超时函数可以在原有保护条件之后调用共享判定器private handlePracticalTimeout( command: PracticalCommand ): void { if (!this.isPracticalExamActive) { return } if (this.currentPracticalCommand?.id ! command.id) { return } if (this.currentPracticalResolved) { return } const verdict evaluateLowStateAtTimeout( command.action, this.currentLightState, acceptsExistingLowAtTimeout(command) ) this.completeCurrentPracticalCommand( verdict.passed, verdict.code ) }落地时仍要适配当前Index.ets的真实函数与收口入口。按这个边界修改后p_meet在超时点已经是近光时会通过当前不是近光时仍失败。其他实操指令继续按原有超时失败路径收口。这里还有一个容易混淆的选择是否在p_meet展示出来的瞬间立刻判正确这次改动不应顺手加入“展示即完成”。普通模式的现有行为是在超时回调中检查已有近光状态本次目标是让两条超时分支对齐。若实操模式一展示命令就自动跳到下一题会改变学员阅读时间、语音播放、倒计时和页面反馈影响范围明显更大。因此建议行为是学员主动点击近光继续由handlePracticalAction提前完成。学员没有点击但近光始终保持到超时点p_meet由共享判定器判正确。学员在等待期间把近光切成其他灯态超时点不满足判失败。其他命令没有明确放开已有状态继续按原路径处理。这是一处局部对齐不等于重新定义整个实操考试的交互节奏。从命令读取到单次提交的端到端接线把纯判断放回页面时可以把一次超时处理拆成“读取命令、过滤旧回调、读取灯态、计算结果、单次提交”五段。这样排查时能定位到底是命令过期、灯态错误还是结果被重复写入而不用从最后一个布尔值向前猜。阶段输入必须守住的条件输出读取命令当前实操命令与回调命令编号两个编号必须一致当前有效命令过滤会话活动标记与已收口标记会话活动且尚未收口是否继续读取灯态页面统一灯态在有效回调中读取最新值currentLightState计算结果动作、灯态、命令策略只有p_meet允许已有近光TimeoutVerdict单次提交判定结果与原因码提交前再次检查收口标记一次提示、计分和切题下面的 ArkTS 示例把这五段接在同一个入口中。它继续复用前文的evaluateLowStateAtTimeout没有另造第二套判断规则函数名和状态字段仍需按真实页面适配。private handlePracticalTimeout(commandId: string): void { if (!this.isPracticalExamActive || this.currentPracticalResolved) { return } const command: PracticalCommand | undefined this.currentPracticalCommand if (command undefined || command.id ! commandId) { return } const currentLightState: string this.currentLightState const acceptExistingLow: boolean command.id p_meet command.action ACTION_LOW const verdict: TimeoutVerdict evaluateLowStateAtTimeout( command.action, currentLightState, acceptExistingLow ) if (this.currentPracticalResolved) { return } this.completeCurrentPracticalCommand( verdict.passed, verdict.code ) }这段接线把旧回调过滤放在灯态读取和结果提交之前并在提交前再次检查单次收口标记。它没有声称当前工程已经存在currentPracticalResolved若真实页面使用会话序号或索引变化保证幂等应沿用原机制并用并发回归验证而不必重复新增字段。八、用差异回归证明只改变一个业务格子修复后的回归不应只写一句“p_meet通过”。更有价值的做法是列出修改前后矩阵明确哪些结果必须变化哪些结果必须保持。场景修改前建议结果是否允许变化普通模式ACTION_LOW LIGHT_LOW超时通过通过不允许变化普通模式ACTION_LOW LIGHT_HIGH超时失败失败不允许变化普通模式其他动作 LIGHT_LOW超时失败失败不允许变化实操p_meet LIGHT_LOW超时失败通过本次唯一目标变化实操p_meet LIGHT_HIGH超时失败失败不允许变化其他实操命令使用ACTION_LOW超时失败失败暂不改变旧命令的延迟回调到达忽略忽略不允许变化点击已完成后超时回调到达不应重复收口不应重复收口不允许变化这张表可以直接转换为参数化测试interface PracticalTimeoutCase { name: string command: PracticalCommand lightState: string expectedPassed: boolean } const practicalCases: PracticalTimeoutCase[] [ { name: p_meet 已经是近光, command: { id: p_meet, text: 夜间与机动车会车保证近光灯状态, action: ACTION_LOW, category: 会车 }, lightState: LIGHT_LOW, expectedPassed: true }, { name: p_meet 当前不是近光, command: { id: p_meet, text: 夜间与机动车会车保证近光灯状态, action: ACTION_LOW, category: 会车 }, lightState: LIGHT_HIGH, expectedPassed: false }, { name: 其他近光动作没有获得状态豁免, command: { id: p_other_low, text: 执行近光灯操作, action: ACTION_LOW, category: 其他场景 }, lightState: LIGHT_LOW, expectedPassed: false } ] practicalCases.forEach((item: PracticalTimeoutCase) { const verdict evaluateLowStateAtTimeout( item.command.action, item.lightState, acceptsExistingLowAtTimeout(item.command) ) expect(verdict.passed).assertEqual(item.expectedPassed) })第三条尤其重要。它证明本次修改依据的是p_meet的具体语义而不是看到ACTION_LOW就批量改变所有实操命令。还需要验证旧回调隔离。假设p_meet的定时器触发时页面已经切换到下一条指令回调中的灯态可能刚好是近光。如果先调用判定器、后比较命令编号就可能把上一题结果写到下一题。因此顺序必须是确认实操会话仍处于活动状态。确认回调携带的命令编号仍是当前编号。确认当前命令尚未收口。读取当前灯态并调用判定器。执行一次终态提交。可以给终态提交再加一道单次保护private completeCurrentPracticalCommand( passed: boolean, code: TimeoutVerdictCode ): void { if (this.currentPracticalResolved) { return } this.currentPracticalResolved true this.clearPracticalTimer() // 以下继续调用项目现有的提示、计分和切题逻辑。 this.applyPracticalResult(passed, code) }如果当前项目已经通过活动标记、索引变化或计时器清理保证单次收口应优先复用现有机制不必为了示例重复增加字段。关键是用测试确认“动作回调与超时回调相邻到达时只提交一次”。九、排查时不要只盯着最后一个布尔值即使判定器只有三项输入接入页面后仍可能遇到“逻辑看起来正确结果却没有变化”的情况。下面列出这类修改中较常见的原因。现象优先检查常见原因处理方向p_meet已经是近光仍失败传入判定器的currentLightState读取的是旧字段或局部副本在超时回调内读取当前统一灯态p_meet任何状态都通过判定条件是否同时比较动作与灯态使用了 action ACTION_LOW其他近光实操题也通过acceptExistingLow的来源直接对所有ACTION_LOW传入true先限定command.id p_meet一题增加两次结果动作与超时的收口保护计时器已进入消息队列单纯清理不足在终态提交处增加幂等保护普通模式原来通过的场景变失败普通入口的第三个参数抽取后误传false用普通模式三组回归锁定切到下一题后上一题回调改写结果命令编号检查顺序判定和写入发生在旧回调过滤之前先比较活动状态与命令编号页面提示与passed相反收口函数的参数顺序新增原因码后调用参数错位使用具名结果对象减少位置错误用例通过但真机操作节奏异常是否加入了展示即完成修复范围扩大到命令呈现阶段恢复为仅在超时点读取状态调试日志也应围绕这三个输入和一次提交而不是打印整个页面对象。建议记录命令编号、期望动作、当前灯态、策略开关、结果原因和会话序号interface TimeoutTrace { sessionId: string commandId: string expectedAction: string currentLightState: string acceptExistingLow: boolean verdictCode: TimeoutVerdictCode } function createTimeoutTrace( sessionId: string, command: PracticalCommand, lightState: string, verdict: TimeoutVerdict ): TimeoutTrace { return { sessionId, commandId: command.id, expectedAction: command.action, currentLightState: lightState, acceptExistingLow: acceptsExistingLowAtTimeout(command), verdictCode: verdict.code } }这类结构化记录可以帮助判断问题出在题库数据、灯态读取、策略选择还是最终写入。正式接入时应沿用项目已有日志设施并避免写入用户可识别信息。如果看到verdictCode已经是SATISFIED_BY_LOW_STATE页面却仍显示失败说明纯判断已经完成问题应继续追到收口调用和页面状态更新而不应反复修改evaluateLowStateAtTimeout。十、验证清单与交付边界在真正修改源码后可以按下面顺序验证。前半部分是纯判断后半部分才涉及页面和设备。纯判断与分支回归ACTION_LOW LIGHT_LOW acceptExistingLowtrue返回通过。ACTION_LOW 非 LIGHT_LOW acceptExistingLowtrue返回失败。非 ACTION_LOW LIGHT_LOW acceptExistingLowtrue返回失败。ACTION_LOW LIGHT_LOW acceptExistingLowfalse返回失败。普通模式抽取前后的三组输出保持一致。p_meet LIGHT_LOW从实操旧结果失败变为建议结果通过。p_meet 非 LIGHT_LOW仍为失败。其他实操命令没有被连带放行。旧命令的延迟回调不会写入当前命令。动作完成与超时到达相邻发生时只提交一次结果。页面联调先执行一个结束后保持LIGHT_LOW的前置操作。进入p_meet时不再次点击近光按钮。等待本题超时确认本题只生成一次正确结果。进入p_meet后切换到远光再等待超时确认结果为失败。主动点击近光时仍可沿用原动作处理路径提前完成。普通灯光模式的近光超时行为没有变化。提示文本、得分、进度和历史记录使用同一个最终结果。快速切题后上一题定时器不会影响下一题。当前事实、建议方案与尚未完成事项层级本文结论当前源码事实p_meet的文案为“夜间与机动车会车保证近光灯状态”动作字段为ACTION_LOW分类为会车当前源码事实普通handleTimeout对ACTION_LOW LIGHT_LOW保留了超时通过分支当前源码事实实操handlePracticalTimeout在有效回调进入后没有对应状态分支会按失败收口建议方案抽取最小的evaluateLowStateAtTimeout普通模式保持现状实操模式只为p_meet打开已有近光满足条件建议方案保留会话活动、命令编号和单次收口保护用差异回归证明只改变目标格子尚未完成本文代码没有写入Index.ets或其他项目源码尚未完成没有运行项目构建没有生成新的 HAP尚未完成没有在模拟器或真机上重放p_meet场景尚未完成没有形成安装包运行、触控过程或历史记录回读证据源码阅读能确认的是两条超时分支当前存在不一致建议代码说明的是一种局部对齐方法。只有完成实现、回归、构建和设备操作后才能确认最终交互是否符合产品约定。因此这里只将它列为待实施方案不把静态分支分析冒充为已经落地或通过真机验证的结果。
返回列表