一、复盘页要回答事实而不是拼接文案离线训练报告至少要回答四个问题这一周完成了什么、实际投入多少时间、原计划有哪些偏差、下周该保留或调整什么。若页面只把累计分钟、收藏数和一句建议拼成文本用户无法追溯每个结论来自哪条记录跨周后也无法稳定回看。本方案把训练记录视为不可变事实把周计划视为可修改目标把报告视为某一时间窗口的派生快照。全部核心计算在本地完成不依赖网络云同步、教练点评和医学建议不属于当前范围。风险提示只提供训练负荷提醒不替代专业诊断。数据类型是否可修改主键主要用途TrainingRecord完成后只允许备注修正recordId统计实际训练事实PlanTask本周内可调整weekKey taskId表达目标与完成状态WeeklyReport生成后可重算新版本weekKey revision快速回看与导出ProfileSnapshot生成时固定reportId解释建议所依据的画像二、先统一周窗口和日期键“本周”不能依赖页面打开当天临时计算。数据层用本地时区生成weekKey并明确周一为起点记录保存原始时间戳同时派生日期键。跨时区或用户修改系统时间时旧记录仍保留原始键报告重算需要提示时间范围变化。export interface WeekWindow { weekKey: string startEpochMs: number endEpochMs: number timezoneOffsetMinutes: number } export interface TrainingRecord { recordId: string sessionId: string courseId: string moduleId: string durationMinutes: number rating: number completedAt: number localDateKey: string weekKey: string }sessionId用于消除重复完成记录weekKey用于快速查询completedAt则是重新归档时的最终依据。周窗口实现要单测月末、年末和夏令时地区如果应用只面向固定地区也应把时区假设写入模型而不是散落在页面格式化代码里。三、计划快照保留目标变化的来龙去脉周计划任务需要同时保存计划时长、完成记录引用和更新时间。用户把周二课程改到周三时系统不应删除原任务后重新创建否则报告无法解释计划变化。推荐使用修订号与rescheduledFrom保存调整链。export interface WeeklyPlanTask { taskId: string weekKey: string weekday: number courseId: string plannedMinutes: number status: planned | done | skipped | replaced completedRecordId?: string rescheduledFrom?: string updatedAt: number revision: number } export interface WeeklyPlan { weekKey: string tasks: WeeklyPlanTask[] generatedFrom: default | assessment | manual updatedAt: number }计划完成状态由completedRecordId和状态共同约束。训练记录存在但计划仍为 planned 时仓库执行补偿关联记录被移除或损坏时任务回退为 planned 并留下诊断而不是保留一个无法打开的“已完成”标记。四、聚合器输出指标和解释依据报告聚合器接收一周的记录、计划和画像快照输出总时长、训练天数、模块覆盖、计划完成率与风险提示。它不读取 UI 状态也不负责保存。相同输入必须产生相同结果方便回归测试和以后迁移到 Worker 或 TaskPool。export class WeeklyReportAggregator { build(input: ReportInput): WeeklyReportDraft { const records dedupeBySession(input.records) const totalMinutes sum(records.map(item item.durationMinutes)) const activeDays unique(records.map(item item.localDateKey)).length const planned input.plan.tasks.filter(item item.status ! replaced) const completed planned.filter(item item.completedRecordId) const moduleMinutes groupMinutesByModule(records) return { weekKey: input.window.weekKey, totalMinutes, activeDays, planCompletionRate: ratio(completed.length, planned.length), moduleMinutes, highlights: buildHighlights(records, moduleMinutes), risks: buildRiskHints(records, input.profile) } } }风险规则要可解释。例如连续三天高强度训练触发提醒时报告保存触发规则标识和相关记录 ID详情页可展开查看依据。规则没有足够数据时返回“数据不足”不能用默认值生成看似精确的结论。五、报告仓库保存版本而非覆盖唯一文本同一周可能多次生成报告。仓库为每次结果增加修订号默认展示最新版本同时允许用户回看上一版。数据结构升级时先迁移记录和计划再重算报告旧报告可保留为只读快照避免迁移失败导致历史内容全部消失。export class WeeklyReportRepository { async save(draft: WeeklyReportDraft): PromiseWeeklyReport { const previous await this.findLatest(draft.weekKey) const report: WeeklyReport { ...draft, reportId: createId(), revision: (previous?.revision ?? 0) 1, generatedAt: this.clock.now(), schemaVersion: CURRENT_SCHEMA } await this.storage.transaction(async tx { await tx.put(report:${report.reportId}, report) await tx.put(report_latest:${report.weekKey}, report.reportId) }) return report } async findLatest(weekKey: string): PromiseWeeklyReport | null { return await this.storage.resolveLatestReport(weekKey) } }结构化记录数量持续增长后更适合使用关系型存储少量设置和最新报告指针可以继续使用轻量键值存储。无论选哪一种页面都不直接解析 JSON也不能在渲染函数中顺手修复数据。六、离线优先也要设计失败画面本地读取并不等于永远成功。存储空间不足、JSON 损坏、事务中断、时区变化和模式升级都可能发生。报告页必须区分“本周无训练”“数据正在迁移”“部分记录不可用”和“报告生成失败”并保留查看原始记录或重试的入口。失败条件数据策略页面状态禁止动作本周没有记录返回空报告草稿展示开始训练入口不生成虚假趋势一条记录损坏隔离该条并记录诊断提示部分数据未计入不清空其他记录报告保存失败保留内存中的草稿提供重新保存不重复写训练记录时区发生变化标记需确认的周窗口展示新旧范围不静默移动历史记录版本迁移失败回滚到只读旧数据提供导出与重试不覆盖原始快照七、回看体验围绕“周”组织页面顶部提供上一周、下一周和回到本周三个动作中部按计划完成率、训练时长、模块覆盖展示结构化卡片底部列出产生结论的训练记录。点击指标必须能回到具体记录避免报告成为不可解释的宣传数字。窄屏采用单列平板可把计划和实际统计并排。加载历史周时保留当前周内容并显示刷新状态避免整个页面闪成空白。导出只输出用户主动选择的周报告不默认包含画像中的伤病或身体信息。八、分阶段实施和证据闭环第一阶段固定周窗口与结构化记录第二阶段补齐计划任务关联和幂等修复第三阶段实现纯聚合器与报告版本第四阶段增加历史周导航、迁移与导出。每阶段都以旧数据可读、其他周不受影响为前提。验收至少包含零记录、一条记录、重复 session、多模块、跨周完成、年末跨周、计划改期、存储失败和版本迁移。后续证据计划包括聚合器单测、真机离线重启录屏、周切换截图、迁移前后数据对照与失败注入日志。只有这些运行材料齐备后才能把方案标题升级为实战。状态持久化能力的边界可参考华为开发者文档。九、总结离线训练报告的可信度来自可追溯的数据链训练会话形成事实记录周计划表达目标聚合器生成可解释指标报告仓库保存版本。把文本展示建立在这条链路之上用户才能在无网络环境下稳定回看每周变化开发者也能对重复、迁移和损坏场景给出明确处理。