一、倒计时显示不是计时器设计训练页上的数字每秒减一只能说明前台定时回调正在执行。锁屏、切后台、系统冻结进程或页面重建后回调次数不再等于真实经过时间如果继续把remainingSeconds当成唯一真相恢复时就可能多算、少算甚至重复生成训练记录。本方案把主动训练定义为一个可持久化会话开始、运行、暂停、恢复、完成和放弃都是明确事件。倒计时只是由状态推导出的视图。范围包含前台训练、短时后台、应用重进和手动结束不承诺系统强杀后持续后台播报也不把语音播放状态等同于训练状态。状态可接受事件时间是否推进持久化要求readySTART、ABANDON否可不写快照runningTICK、PAUSE、FINISH是写入时间基准pausedRESUME、FINISH、ABANDON否写入剩余秒数finishedSAVE_RECORD、DISMISS否记录必须幂等abandonedDISMISS否清理临时快照二、会话模型保存时间基准而非回调次数运行态需要保存deadlineEpochMs暂停态保存remainingSeconds。页面每次刷新都用当前时间计算剩余值而不是依赖定时器精确触发。这样前台卡顿十秒后视图会直接跳到正确时间恢复应用时也能根据快照重新推导状态。export type TrainingPhase ready | running | paused | finished | abandoned export interface TrainingSession { sessionId: string courseId: string phase: TrainingPhase totalSeconds: number remainingSeconds: number deadlineEpochMs: number startedAt: number pausedAt: number finishedAt: number recordCommitted: boolean revision: number }sessionId用来阻止完成按钮重复写入记录revision用来判断旧快照是否覆盖新状态。语音提示、当前动作序号和 UI 展开状态可以附加保存但它们不是计算剩余时间的必要字段恢复失败时可以降级为文字步骤而训练时长仍保持正确。三、所有事件经过同一个归约器页面按钮、生命周期回调和系统媒体控制不直接修改多个布尔值而是提交事件。归约器验证当前状态是否允许该事件并返回一份新会话。非法事件被忽略并记录诊断例如暂停态再次暂停、完成态再次完成。export type TimerEvent | { type: START; now: number } | { type: PAUSE; now: number } | { type: RESUME; now: number } | { type: FINISH; now: number } | { type: ABANDON; now: number } export function reduceTimer(state: TrainingSession, event: TimerEvent): TrainingSession { if (event.type START state.phase ready) { return startSession(state, event.now) } if (event.type PAUSE state.phase running) { return pauseSession(state, event.now) } if (event.type RESUME state.phase paused) { return resumeSession(state, event.now) } if (event.type FINISH (state.phase running || state.phase paused)) { return finishSession(state, event.now) } return state }归约器保持纯函数持久化和语音副作用放在 Controller 中。这样可以用固定时间输入测试边界不必真的等待一分钟。UI 也只根据phase决定显示“暂停”“继续”或“保存记录”避免按钮文案反过来充当业务状态。四、暂停与恢复围绕绝对时间换算暂停时先用截止时间减去当前时间得到不可为负的剩余秒数然后清空截止时间。恢复时用剩余秒数重建新的截止时间。如果后台时间已经超过截止点恢复入口直接进入完成态不再启动新的定时器。function pauseSession(session: TrainingSession, now: number): TrainingSession { const left Math.max(0, Math.ceil((session.deadlineEpochMs - now) / 1000)) return { ...session, phase: left 0 ? finished : paused, remainingSeconds: left, deadlineEpochMs: 0, pausedAt: now, finishedAt: left 0 ? now : 0, revision: session.revision 1 } } function resumeSession(session: TrainingSession, now: number): TrainingSession { return { ...session, phase: running, deadlineEpochMs: now session.remainingSeconds * 1000, pausedAt: 0, revision: session.revision 1 } }时间源应封装为Clock.now()单元测试才能覆盖跨分钟、跨日和系统时间回拨。若产品对时间回拨敏感可同时记录单调时钟差值若只需要普通训练计时检测到异常回拨时暂停会话并提示用户确认是更稳妥的降级。五、快照仓库承担重进恢复和损坏隔离会话快照体积很小可以写入轻量持久化但必须校验版本、总时长、剩余时长和课程标识。解析失败不能自动生成一条“已完成”记录默认回到 ready并保留损坏快照的诊断信息。保存应在开始、暂停、恢复、完成和应用退后台时执行。export class TrainingSessionRepository { async save(session: TrainingSession): Promisevoid { const value JSON.stringify(session) await this.store.put(active_training_session, value) await this.store.flush() } async restore(): PromiseTrainingSession | null { const raw await this.store.get(active_training_session, ) if (typeof raw ! string || raw.length 0) return null const parsed JSON.parse(raw) as TrainingSession if (!parsed.sessionId || parsed.totalSeconds 0) return null if (parsed.remainingSeconds 0 || parsed.remainingSeconds parsed.totalSeconds) return null return { ...parsed } } async clear(): Promisevoid { await this.store.delete(active_training_session) await this.store.flush() } }六、后台、语音与系统控制分层降级训练计时可以继续依据绝对时间恢复但后台语音、媒体会话和长时任务受设备能力、权限与系统策略影响。启动后台任务失败时不应停止训练会话只关闭语音并保留通知或文字提示。媒体控制的 pause/play 事件要提交到同一归约器不能只暂停播报而让训练倒计时继续。异常会话处理语音处理用户反馈后台任务启动失败继续 running降级为关闭提示前台训练仍可用TTS 初始化失败不改时间使用文字步骤显示语音暂不可用页面重建从快照恢复按设置重新初始化回到对应暂停或运行态快照损坏回到 ready不启动提示重新开始训练完成按钮连点只提交一次记录停止播报保持完成结果七、记录提交必须与完成状态解耦完成会话与写入训练记录是两个步骤。会话先进入 finished再由commitRecord(sessionId)执行幂等写入写入成功后把recordCommitted设为 true。若持久化失败完成页保留“重试保存”不能重新开始计时或生成新的会话标识。计划任务的完成标记也应和同一个sessionId绑定。记录成功、计划标记失败时允许单独补偿两者都成功后才能清理活动会话快照。这样弱存储或异常退出不会产生两条训练记录也不会把计划误标为未完成。八、实施与验收按风险递增实施顺序建议为先引入会话模型和纯归约器再把前台开始、暂停、继续接入随后增加快照恢复最后连接后台任务、TTS 与媒体控制。每一步都保留文字训练步骤外部能力失败时不会阻断主流程。验收需要覆盖状态转换表中的每一条合法边和至少六条非法边前台停留、切后台三十秒、页面重建、应用重启、完成按钮连点、快照损坏都要有可观察结果。后续证据计划包含状态机单测、真机前后台录屏、恢复前后时间对照、记录幂等日志和后台能力失败截图。在这些证据形成前本文保持设计定位。状态驱动 UI 的基础机制可参考华为开发者文档。九、总结可恢复训练计时器的核心是“事件决定状态绝对时间决定剩余值快照决定重进恢复”。把训练状态、语音副作用和记录提交分开后暂停、继续、后台降级与重复点击才有一致的处理方式也为后续真机验证留下了明确、可量化的验收入口。