跳到主要内容

复盘页研发拆解

基于 完整方案评审简版,按 6 个模块拆解研发任务。每个模块包含目标、输入输出、依赖、风险、是否必须实现、验收点。


模块 1:统一数据模型

目标

定义 6 个核心数据模型的 Dart class + SQLite 建表 SQL,作为手表采集和复盘页消费的统一数据契约。

输入输出

输入输出
完整方案 Section D 的 6 个模型定义lib/models/ 下 6 个 Dart 文件
现有 DotRecord/RecordingData 结构database_service.dart DB v14 迁移
-toMap/fromMap 序列化方法

新增文件

文件内容
lib/models/session.dartSession 模型(id, start_time, end_time, pauses, sport_type, device_sources, status, clock_offset_ms)
lib/models/event_marker.dartEventMarker 模型(在 DotRecord 基础上增加 confidence, weight, is_revoked, tags)
lib/models/sensor_sample.dartSensorSample 模型(timestamp, heart_rate, speed, distance_increment, lat/lng, quality_flag)
lib/models/segment.dartSegment 模型(start/end_time, segment_type, intensity_level, avg_heart_rate, linked_events)
lib/models/replay_anchor.dartReplayAnchor 模型(source_type, anchor_time, display_time, linked_video_path, linked_event_ids)
lib/models/quality_state.dartQualityState 模型(data_type, quality_level, reason, missing_ranges, confidence_score)

DB v14 新增表

-- 训练会话
CREATE TABLE sessions(
id TEXT PRIMARY KEY,
start_time INTEGER NOT NULL,
end_time INTEGER,
pauses TEXT, -- JSON: [[pause_ms, resume_ms], ...]
sport_type TEXT NOT NULL,
device_sources TEXT NOT NULL, -- JSON: ["phone","watch"]
status INTEGER NOT NULL DEFAULT 0, -- 0=active, 1=paused, 2=completed, 3=interrupted
clock_offset_ms INTEGER,
recording_id INTEGER,
video_path TEXT,
FOREIGN KEY (recording_id) REFERENCES recording_records(id)
);

-- 传感器采样(核心大表)
CREATE TABLE sensor_samples(
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
heart_rate INTEGER,
speed REAL,
distance_increment REAL,
latitude REAL,
longitude REAL,
altitude REAL,
quality_flag INTEGER NOT NULL DEFAULT 0, -- 0=good, 1=degraded, 2=interpolated, 3=unavailable
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE INDEX idx_sensor_session_time ON sensor_samples(session_id, timestamp);

-- 语义区段(客户端计算生成)
CREATE TABLE segments(
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
segment_type INTEGER NOT NULL, -- 0=intensity, 1=sprint, 2=rest, 3=warmup
intensity_level INTEGER, -- 0=light, 1=moderate, 2=vigorous, 3=max
avg_heart_rate INTEGER,
peak_speed REAL,
avg_speed REAL,
distance REAL,
linked_events TEXT, -- JSON: [event_id, ...]
source INTEGER NOT NULL DEFAULT 0, -- 0=auto, 1=user, 2=ai
confidence REAL NOT NULL DEFAULT 1.0,
FOREIGN KEY (session_id) REFERENCES sessions(id)
);

-- 数据质量状态
CREATE TABLE quality_states(
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
data_type INTEGER NOT NULL, -- 0=heart_rate, 1=speed, 2=gps, 3=event, 4=session
quality_level INTEGER NOT NULL, -- 0=good, 1=degraded, 2=unavailable
reason TEXT,
missing_ranges TEXT, -- JSON: [[start_ms, end_ms], ...]
confidence_score REAL NOT NULL DEFAULT 1.0,
sample_count INTEGER,
expected_count INTEGER,
FOREIGN KEY (session_id) REFERENCES sessions(id)
);

依赖

无外部依赖。可独立开发。

风险

风险缓解
sensor_samples 数据量大(5400 条/场)添加复合索引 (session_id, timestamp),查询 benchmark 目标 200ms 以内
EventMarker 与 DotRecord 双模型共存EventMarker 继承 DotRecord 字段 + 增强字段,回放页继续用 DotRecord,复盘页用 EventMarker

当前阶段是否必须实现

是。阻塞所有后续模块。

验收点

  • 6 个 Dart 模型文件创建,toMap/fromMap 可用
  • DB v14 迁移 SQL 执行成功,旧数据不受影响
  • sensor_samples 写入 5400 条 + 按 session_id 查询耗时测试通过

模块 2:手表端传感器采集

目标

WearOS 和 watchOS 手表端新增心率连续采集能力,与现有打点功能并行运行。录制开始时自动启动心率采集,录制结束时停止并将数据传回手机。

输入输出

输入输出
录制状态(state_update 消息)心率数据流(批量或逐条传回手机)
手表内置心率传感器 APISession 生命周期事件
-时钟偏移量(首次同步时计算)

WearOS 实现 (blinklife-wear/)

新增类职责
HeartRateCollector.kt使用 Health Services API 订阅心率,1Hz 采样,缓存到本地 List
SessionManager.kt管理 Session 生命周期,接收 state_update 触发 start/pause/stop
SensorDataSender.kt批量发送心率数据到手机(每 30 秒或录制结束时 flush)

通信协议扩展

// 新增消息路径
/sensor_data → 手表→手机,批量心率数据
/time_sync → 双向,时钟同步

// sensor_data 格式
{
"type": "sensor_batch",
"session_id": "uuid",
"samples": [
{"ts": 1712956800000, "hr": 145},
{"ts": 1712956801000, "hr": 148},
...
]
}

// time_sync 格式(手机发起)
{
"type": "time_sync_request",
"phone_time": 1712956800000
}
// 手表回复
{
"type": "time_sync_response",
"phone_time": 1712956800000,
"watch_time": 1712956800350 // 偏移 350ms
}

watchOS 实现 (ios/BlinkLifeWatch/)

新增类职责
HeartRateCollector.swift使用 HealthKit HKWorkoutSession 订阅心率
SessionManager.swiftSession 生命周期
SensorDataSender.swift通过 WCSession.transferUserInfo 批量传输

Flutter 侧接收

修改文件变更
watch_communication_service.dart新增 onSensorBatch Stream,处理 /sensor_data 消息
dot_input_manager.dart新增 _onSensorBatch() 方法,写入 DB

依赖

依赖说明
模块 1(数据模型)SensorSample 模型和 DB 表
WearOS Health Services API需要 Wear OS 3.0+,需声明 BODY_SENSORS 权限
watchOS HealthKit需要 HealthKit entitlement + 用户授权

风险

风险缓解
心率传感器不同手表硬件差异大只采集心率(最通用),不采集加速度计原始数据
批量传输数据量大(30 秒 = 30 条)JSON 压缩,每条仅 ts+hr 两个字段,30 条约 1KB
HealthKit 用户拒绝授权降级:QualityState.heart_rate = unavailable,复盘页隐藏心率模块
手表电量消耗增加心率传感器本身功耗低(手表运动模式标配),主要注意传输频率

当前阶段是否必须实现

是。心率是复盘页 MVP 的核心数据源。

验收点

  • WearOS:录制 30 分钟,心率数据完整传回手机(覆盖率大于 90%)
  • watchOS:同上
  • 时钟偏移计算结果在 ±2 秒以内
  • 手表端功耗:30 分钟采集电量下降不超过 5%
  • 用户拒绝 HealthKit 授权后不崩溃,优雅降级

模块 3:同步落库

目标

手表传回的心率数据和 Session 事件,经过清洗后写入本地 SQLite,同时计算并存储 QualityState。

输入输出

输入输出
WatchCommunicationService.onSensorBatchsensor_samples 表中的数据行
Session 生命周期事件sessions 表记录
time_sync 偏移量sessions.clock_offset_ms
-quality_states 表记录

新增 Service

文件职责
lib/services/sensor_data_service.dart统一管理传感器数据写入/查询/聚合

核心方法

SensorDataService (单例)

├── writeBatch(sessionId, List of SensorSample)
│ → 批量写入 sensor_samples 表
│ → 异常值过滤(心率超过 0-220 范围标记 degraded)

├── getHeartRateSeries(sessionId) → List of (timestamp, hr)
│ → 查询 + 过滤 quality_flag != unavailable

├── getAggregates(sessionId) → {avgHR, maxHR, minHR, totalDistance}
│ → 聚合统计

├── computeQualityState(sessionId, dataType) → QualityState
│ → 计算覆盖率 = sample_count / expected_count
│ → 检测缺失区间(连续 30 秒无数据 = missing_range)
│ → 设置 quality_level: 覆盖率大于80%=good, 40-80%=degraded, 低于40%=unavailable

└── cleanup(sessionId)
→ 清除过期/临时数据

依赖

依赖说明
模块 1(数据模型)SensorSample, QualityState 模型
模块 2(手表采集)数据输入源
database_service.dartDB 读写

风险

风险缓解
批量写入 I/O 阻塞 UI使用 sqflite 的 batch 事务 + compute isolate
数据重复写入(手表重传)sensor_samples 用 (session_id, timestamp) 做 UNIQUE 约束,INSERT OR IGNORE
QualityState 计算耗时延迟到录制结束后一次性计算,不实时

当前阶段是否必须实现

是。采集了不落库等于没采集。

验收点

  • 30 分钟录制的 1800 条心率数据全量写入,无丢失
  • 重复数据不报错(INSERT OR IGNORE)
  • QualityState 计算结果正确(手动制造断点验证覆盖率和 missing_ranges)
  • 聚合查询(avgHR, maxHR)耗时低于 200ms

模块 4:时间轴对齐

目标

保证手表打点时间、手表心率时间、手机视频时间三条时间线可以精确对齐,使"点击第 12 分钟心率峰值"能准确跳转到视频第 12 分钟画面。

输入输出

输入输出
Session.clock_offset_ms(手表-手机偏移)对齐后的 display_time
RecordingRecord.recordingStartTimeReplayAnchor.display_time
视频 creation_time 元数据-
RecordingData.alignOffsetMs(手动对齐偏移)-

对齐公式

// 手表事件 → 视频时间
watch_event_time_in_video =
(watch_event_timestamp - clock_offset_ms) // 校正为手机时间
- actualRecordingStartTime // 减去录制开始时间
+ alignOffsetMs // 加手动对齐偏移

// 其中
actualRecordingStartTime = video_creation_time - video_duration

实现位置

修改文件变更
lib/services/video_metadata_service.dart新增 alignWatchTime(watchTimestamp, clockOffset, startTime, alignOffset) → Duration
lib/models/replay_anchor.dartReplayAnchor.fromEvent() / ReplayAnchor.fromHeartRatePeak() 工厂方法内置对齐计算

依赖

依赖说明
模块 2 的 time_syncclock_offset_ms 值
现有 VideoMetadataServicecreation_time 获取
现有 alignOffsetMs 机制手动对齐偏移

风险

风险缓解
运动中手表时钟漂移录制首尾各同步一次,取平均;漂移超过 3 秒标记 QualityState.session = degraded
视频缺少 creation_time前置检查已有(VideoMetadataService.hasCreationTime),失败时禁止跳转
多路偏移叠加误差放大单元测试覆盖:给定已知偏移,验证最终 display_time 精度在 ±1 秒

当前阶段是否必须实现

是。对齐不准 = 跳转错位 = 体验崩溃。

验收点

  • 手表打点 + 手机录制,打点视频时间对齐误差在 ±2 秒以内
  • 心率峰值跳转到视频对应时间,画面与峰值时刻匹配
  • 无 creation_time 的视频,跳转按钮灰显 + 提示文案
  • 单元测试:给定 clock_offset=500ms, alignOffset=2000ms,验证计算结果

模块 5:复盘页消费

目标

构建复盘页 UI(5 个模块)+ ReplayAnchor 联动回放页。

输入输出

输入输出
Session + EventMarker + SensorSample + QualityState复盘页 5 个模块 UI
ReplayAnchor跳转回放页的导航参数

新增文件

文件职责
lib/screens/review_page.dart复盘页主容器
lib/widgets/review_overview_card.dart总览卡片
lib/widgets/review_event_distribution.dart事件分布图
lib/widgets/review_heart_rate_chart.dart心率趋势曲线
lib/widgets/review_ai_summary.dartAI 摘要卡片
lib/widgets/review_recommended_clips.dart推荐片段列表
lib/services/review_data_service.dart复盘页数据聚合 Service

ReviewDataService 核心方法

ReviewDataService

├── loadReviewData(sessionId) → ReviewPageData
│ → 聚合 Session + Events + Sensor + Quality
│ → 一次性加载,避免多次 DB 查询

├── getEventDistribution(sessionId) → List of (time, type, confidence)

├── getHeartRateSeries(sessionId) → List of (time, hr, quality)
│ → 断点标记(连续 unavailable 超过 30s 插入 null 值画虚线)

├── getRecommendedClips(sessionId, limit: 5) → List of ReplayAnchor
│ → 排序: confidence x weight,心率峰值关联加权

└── buildReplayAnchor(event/peak/segment) → ReplayAnchor
→ 内置时间对齐计算

回放页改动

修改文件变更
lib/screens/review_detail_page.dart构造函数新增 replayAnchorreplayAnchors 可选参数
lib/screens/review_detail_page.dartinitState 检测锚点 → 自动 seek + 高亮
lib/screens/review_detail_page.dart集合浏览模式:上/下条在锚点列表内导航

降级渲染逻辑

渲染每个模块前:
quality = QualityStateService.get(sessionId, dataType)

if quality.level == unavailable:
→ 隐藏该模块(不显示占位符)
elif quality.level == degraded:
→ 显示模块 + 标注"数据不完整"
→ 曲线断点用虚线
else:
→ 正常渲染

依赖

依赖说明
模块 1-4 全部数据模型 + 采集 + 落库 + 对齐
现有回放页跳转目标

风险

风险缓解
复盘页做成回放页副本复盘页禁止视频播放器,严格通过 ReplayAnchor 跳转
心率曲线渲染性能(5400 点)降采样到 100-200 个点做可视化,保留原始数据做聚合
集合浏览模式与现有回放页冲突新增 _browseMode 状态变量,独立控制上/下条导航逻辑

当前阶段是否必须实现

是。但仅实现 5 个 MVP 模块。

验收点

  • 复盘页入口:回放页更多菜单 + 首页卡片
  • 总览卡片数据正确(时长/打点数/心率均值/峰值)
  • 事件分布图点击 → 跳转回放页正确 seek
  • 心率趋势:断点虚线显示,峰值可点击跳转
  • AI 摘要生成完成,包含数据来源标注
  • 推荐片段缩略图显示,点击跳转 + 一键剪辑
  • 心率覆盖率低于 40% 时心率模块隐藏
  • 无视频时跳转按钮灰显

模块 6:AI 约束层

目标

定义 AI 摘要的生成规则,确保输出不超出数据依据。第一版不做 ML 模型,使用规则模板 + LLM prompt 生成。

输入输出

输入输出
Session 元数据2-4 句训练摘要
EventMarker 统计高光推荐排序
心率聚合 (avg/max/zone 分布)强度评价句
QualityState输出约束

新增文件

文件职责
lib/services/ai_review_service.dartAI 摘要生成 Service

生成规则

AiReviewService.generateSummary(ReviewPageData data) → AiSummary

Step 1: 检查数据充分性
if data.events.isEmpty → return "本场未记录到事件,建议下次录制时更频繁标记"
if data.session.duration 低于 15分钟 → 简短版摘要(仅统计)

Step 2: 生成训练概况句
模板: "本场{sportType}训练 {duration} 分钟,共标记 {totalEvents} 个事件"
补充: 打点构成("其中射门 8 次、犯规 3 次")

Step 3: 生成强度评价句(依赖心率)
前置: quality_states.heart_rate.level != unavailable
模板: "整体强度{level},{zone_pct}% 时间处于{zone_name}区"
缺失: 跳过此句

Step 4: 生成高光推荐句
前置: events.length 大于等于 3
模板: "推荐查看第 {time} 分钟的{action},当时心率达到 {hr} bpm"
缺失: "事件较少,无法生成高光推荐"

Step 5: 生成节奏/建议句
前置: session.duration 大于等于 30分钟 且 events.length 大于等于 5
计算: 前半段 vs 后半段事件密度
模板: "上半场事件密集,下半场节奏放缓" 或 "全场节奏均匀"
缺失: 跳过此句

Step 6: 附加数据来源标注
"基于 {eventCount} 个打点事件和 {hrCoverage}% 心率数据生成"

AI 输出格式

AiSummary {
paragraphs: List[string] // 2-4 段文字
data_source_note: string // "基于 23 个打点事件和 87% 心率数据"
confidence: double // 综合可信度
recommended_anchors: List[ReplayAnchor] // 推荐的回看锚点
}

禁止规则(硬编码到 prompt/模板)

禁止模式检测方式
"因为...所以..."正则过滤因果连接词
"你应该..."禁止指令性建议
"质量下降/提升"无基线数据不做对比
"比上次/上周..."MVP 不做跨场次对比
精确数值(HR 覆盖率低于 80%)检查 QualityState,低覆盖率用"约"

依赖

依赖说明
模块 3(落库)聚合数据输入
模块 5(复盘页)展示 AI 摘要的 Widget
QualityState控制哪些句子可以生成

风险

风险缓解
模板化文案用户感觉"不够智能"预留 LLM 接口,第一版用模板,后续接 Claude API
生成内容超出数据依据每步生成前 check QualityState,缺数据则 skip

当前阶段是否必须实现

部分必须。 模板化摘要 + QualityState 约束必须实现。LLM 接口预留但不实现。

验收点

  • 有打点+有心率:生成 4 句摘要,包含概况/强度/高光/节奏
  • 有打点+无心率:生成 2 句摘要,跳过强度评价
  • 打点少于 3 个:不推荐高光,显示引导文案
  • 训练短于 15 分钟:仅统计,不分析节奏
  • 摘要末尾包含数据来源标注
  • 无因果连接词、无指令性建议、无跨场次对比

模块依赖关系

模块 1 (数据模型)

├──→ 模块 2 (手表采集)
│ │
│ └──→ 模块 3 (同步落库)
│ │
│ ├──→ 模块 4 (时间轴对齐)
│ │ │
│ │ └──→ 模块 5 (复盘页消费)
│ │ │
│ │ └──→ 模块 6 (AI 约束)
│ │
│ └──→ 模块 6 (AI 约束 - QualityState 输入)

└──→ 模块 4 (ReplayAnchor 模型)

可并行的组合

  • 模块 1 + 模块 2 的 WearOS/watchOS 原生开发可并行
  • 模块 4 + 模块 6 的规则定义可并行
  • 模块 5 的 UI 骨架可在模块 3 完成前用 mock 数据先搭

总验收清单

#验收项模块
1DB v14 迁移成功,旧数据兼容1
2WearOS 心率采集 30 分钟覆盖率大于 90%2
3watchOS 心率采集 30 分钟覆盖率大于 90%2
4时钟偏移在 ±2 秒以内2, 4
5传感器数据批量写入无丢失3
6QualityState 覆盖率计算正确3
7手表打点→视频跳转对齐误差在 ±2 秒4
8复盘页 5 个模块渲染正确5
9点击事件/心率峰值→回放页跳转精确5
10心率覆盖率低于 40% 时心率模块隐藏5
11AI 摘要包含数据来源标注6
12AI 摘要无因果推断和指令性建议6