|
|
@@ -0,0 +1,111 @@
|
|
|
+# D/X 域桥接契约 · 华为/小米运动健康同步数据桥接五维消费链路(GAP-A 补建)
|
|
|
+
|
|
|
+> **关联裁决**:O-17(GAP-A 契约缺口,2026-11-08 研发负责人先行裁决,处置路径 = 按 O-7 契约补建工作流「仅登记不实现」:先补本设计契约 → 契约合入 origin/cfclub 后转测试侧 TR 补证据 → 桥接实现待本契约拍板后入 TDD 池派发,优先级 P1,不阻塞 241 SC 主线;O-14「D/X 域不纳入 241 基线」口径不变)。
|
|
|
+> **核对方式**:契约核对 = 代码定位(`cfc-backend/src/main/java` 实测)+ 表结构核对(`schema.sql`)+ 前端调用核对(`cfc-frontend/src`),防假红。
|
|
|
+> **实现基线(写侧已落地,消费侧空窗)**:`4344688d2`(feat xiaomi-health X1-X5)、`978d1bc95` + `4815aae95` + `ba6c0ebe6`(feat huawei-health D1-D5 + B-50/B-61 鉴权修复)。
|
|
|
+> **状态**:**仅登记不实现**(契约补建阶段,未派发实现)。
|
|
|
+
|
|
|
+## 1. GAP-A 缺口界定(本契约要解决的问题)
|
|
|
+
|
|
|
+D/X 同步引擎(`HealthSyncServiceImpl` / `XiaomiHealthServiceImpl`)已实现「授权 → token → 拉取 → 归一化 → 写库」,**落点仅为 `health_data_sources`**;但:
|
|
|
+
|
|
|
+- **五维 body.\* 子维得分读侧**:`DimensionScoreServiceImpl.getHealthDimension()` 读 `health_dimension_scores`(`HealthDimensionScoreService.getLatestScores`)→ 暴露于 `POST /api/dimension/overview`、`/api/dimension/radar-data`(deprecated,前端走 `/api/dimension-registry/*`)。**该链路不读 `health_data_sources`**。
|
|
|
+- **健康档案展示读侧**:`POST /api/health/indicators/current` 读 `member_indicator_current`;`POST /api/health/body/detail` 读 `health_indicator`(报告派生)。**均不读 `health_data_sources`**。
|
|
|
+- **`health_data_sources` 现有消费方**:仅管理端 `DimensionAdminController`(`/api/admin/dimension/data-source/*`)。`HealthDataSourceRecordService.getLatest/getRecords`、`Mapper.selectByDimension` 无任何业务调用方;`DimensionScoreServiceImpl` 注入了 `dataSourceRecordService` 但**无任何方法调用(死注入)**(实证:`DimensionScoreServiceImpl.java:73` 仅声明)。
|
|
|
+- **cfc-frontend**:全仓 **0 处** `huawei-health` / `xiaomi-health` 调用(实证 grep,2026-11-08 校验)→ 小程序端无任何 D/X 数据展示/操作入口。
|
|
|
+
|
|
|
+誓词:测试侧不得伪证「同步 → 前端展示」链路(mock 端到端验证仅能覆盖「授权 → token → 拉取 → 映射 → 入库」,桥接链路须以本契约 V/C 验收后放行)。
|
|
|
+
|
|
|
+## 2. 写侧现状(已落地,契约锚点)
|
|
|
+
|
|
|
+### 2.1 华为 `HealthSyncServiceImpl`(`service/huawei/impl/HealthSyncServiceImpl.java`)
|
|
|
+- 校验 dataType 合法性(`HuaweiHealthDataType.of`)→ `isBound`(token ACTIVE)→ `fetchByType` → 取最新点 → `NormCompareEngine` 百分位带归一化 → `BAND_SCORE`(VERY_LOW=20 / LOW=40 / MID=65 / HIGH=80 / VERY_HIGH=90 / UNKNOWN=50,0-100)→ 写 `health_data_sources`。
|
|
|
+- `parsedResult` JSON 含:`dataType`、`sourceType="huawei_health"`、`normalized{band,status,percentile,score,normSource}`、`dimensionCodes=type.getDimensionCodes()`、原始指标字段(按 dataType)。
|
|
|
+- 写入:`HealthDataSourceRecordService.saveRecord` = `recordMapper.insert`(追加写)。
|
|
|
+
|
|
|
+### 2.2 小米 `XiaomiHealthServiceImpl`(`service/xiaomi/impl/XiaomiHealthServiceImpl.java`)
|
|
|
+- 同构链路,`sourceType="xiaomi_health"`,`dimensionCodes + parsedResult JSON` 写法一致。
|
|
|
+
|
|
|
+### 2.3 dataType → body.\* 子维编号映射(华为枚举 `HuaweiHealthDataType.java` / 小米枚举同构,两者一致)
|
|
|
+
|
|
|
+| dataType | 标签 | dimensionCodes(写库字段) | 单位 | 对应 `health_dimension_scores.dimension` |
|
|
|
+|---|---|---|---|---|
|
|
|
+| `step` | 步数 | `body.exercise` | steps | `exercise` |
|
|
|
+| `distance` | 距离 | `body.exercise` | km | `exercise` |
|
|
|
+| `heart_rate` | 心率 | `body.immunity` | bpm | `immunity` |
|
|
|
+| `sleep` | 睡眠 | `body.sleep` | h | `sleep` |
|
|
|
+| `body_weight` | 体重 | `body.nutrition,body.growth` | kg | `nutrition` + `growth` |
|
|
|
+
|
|
|
+> 映射规则:`body.X` → `health_dimension_scores.dimension = X`(剥离 `body.` 前缀)。五维向上收敛:`exercise/immunity/sleep/nutrition/growth` 直接对应;`vision/gut` 本期无 D/X dataType(备选血氧/热量接入时才涉及)。
|
|
|
+
|
|
|
+## 3. 桥接设计契约(V/C 判定依据:契约补建阶段仅登记字段级约定,实现待派发)
|
|
|
+
|
|
|
+### 3.1 字段映射契约:`health_data_sources` → `health_dimension_scores`
|
|
|
+
|
|
|
+| 目标字段 | 来源(`health_data_sources`) | 映射规则 | 备注 |
|
|
|
+|---|---|---|---|
|
|
|
+| `member_id` | `member_id` | 直取 | 同步链路已按 memberId 拉取 |
|
|
|
+| `dimension` | `dimension_codes`(逗号分隔) | 切分后逐码 `body.X` → `X`;须 ∈ {growth,sleep,vision,immunity,nutrition,gut,exercise},未知码丢弃 | 一条 source 记录可产出多行(如 body_weight → nutrition+growth 两行) |
|
|
|
+| `score` | `parsed_result.normalized.score` | 直取(0-100 带归一化分,BAND_SCORE 已产出) | 多源同日冲突:**读最新覆盖**(与原 §3c 语义一致:`getLatest` created_at DESC LIMIT 1) |
|
|
|
+| `percentile` | `parsed_result.normalized.percentile` | 直取(可空) | |
|
|
|
+| `data_source` | `parsed_result.sourceType` | `huawei_health` / `xiaomi_health` 映射为 `REPORT` 语义(设备采集 Auto)→ 建议新增 `DEVICE`;未拍板前暂用 `REPORT` | **待拍板项 ①** |
|
|
|
+| `tier` | — | D/X 设备自动采集 = 可信度 2(AI 分析级),随 data_source 裁决 | **待拍板项 ②** |
|
|
|
+| `raw_data` | `parsed_result`(整条 JSON) | 直存,含 dataType/单位/原始指标 | |
|
|
|
+| `assess_date` | `created_at` 对应采集日 | 取指标采集点日期(HuaweiHealthDataPoint 携带时间戳) | 过期语义沿用 `expire_date` 规则 |
|
|
|
+
|
|
|
+**写入语义**:幂等 upsert(member_id+dimension+assess_date 冲突 → 覆盖),**不得**与报告/手动填分产生的 `health_dimension_scores` 行互相覆盖消灭——按 `tier`/`data_source` 分层保留(报告 tier=1 最高优先级)。**防负向回归**:桥接不得降低既有 REPORT/MANUAL 得分行的有效性(非覆盖语义待拍板)。
|
|
|
+
|
|
|
+### 3.2 消费侧接线契约
|
|
|
+
|
|
|
+1. **`DimensionScoreServiceImpl` 死注入解除**:注入的 `dataSourceRecordService` 必须产生真实调用——新增桥接步骤(建议独立 `HealthDataSourceBridgeService`):同步成功后把本批 `health_data_sources` 行按 §3.1 写入/更新 `health_dimension_scores`;`/api/dimension/overview`、`/api/dimension-registry/*` 由此自然读到 D/X 得分。
|
|
|
+2. **写入触发点(二选一,待拍板项 ③)**:
|
|
|
+ - A:同步链路内(`saveRecord` 后同事务桥接)——实现简单、实时;
|
|
|
+ - B:`health_data_sources` 新行侧车(读时桥接)——无写扩散但多一跳。
|
|
|
+3. **档案展示**:`POST /api/health/indicators/current`(读 `member_indicator_current`)与 `POST /api/health/body/detail`(读 `health_indicator`)**提供 D/X 来源展示**——新增读取源或合并视图,字段口径:`body.*` 得分可溯源 `source=huawei_health/xiaomi_health` + 采集时间 + 原始值(步数/心率/睡眠 h/体重 kg)。
|
|
|
+
|
|
|
+### 3.3 小程序端点契约(新增/复用)
|
|
|
+
|
|
|
+| 编号 | 端点 | 语义 | 鉴权 | 备注 |
|
|
|
+|---|---|---|---|---|
|
|
|
+| B-1 | `POST /api/health/data-sources/bridge/list` | 小程序「健康 > 设备数据」列表:读 `health_data_sources`(分页 + source_type 过滤 + memberId 维度) | 登录必填 401 | 现有 `DimensionAdminController` 无 member 侧读取端点 → 新增 |
|
|
|
+| B-2 | `POST /api/health/data-sources/bridge/sync-status` | 各 dataType 最近同步时间/绑定状态(D-5/X-5 状态聚合,前端展示「上次同步 x 小时前」) | 登录必填 401 | 可复用 `status` 端点聚合 |
|
|
|
+| B-3 | 小程序侧操作入口 | cfc-frontend 新增:绑定页(D-1/X-1 auth-url 跳转)+ 同步按钮(D-3/X-3)+ 状态卡(D-5/X-5) | — | 实现阶段 |
|
|
|
+
|
|
|
+> 前端调用示例(实现阶段):`POST /api/huawei-health/sync {memberId, dataType:"sleep"}` → 返回 `results[]`;随后 `POST /api/dimension/overview` 应能读到 `sleep` 维度带 `source=huawei_health` 的新得分。
|
|
|
+
|
|
|
+### 3.4 鉴权契约
|
|
|
+
|
|
|
+- `JwtInterceptor.LOGIN_REQUIRED_PATHS` 已含 `/api/huawei-health/`、`/api/xiaomi-health/`、`/api/health/plan/`(B-50/B-61 已合入 `cc7b3e00b`/`ba6c0ebe6`)。
|
|
|
+- 新增 `B-1/B-2` 端点一律走 `/api/**` 拦截(默认拒绝 + 显式登录),并在对应 Controller 内 `@RequestAttribute("role")` 角色校验(成员私有数据,家长可查自身/代管)。
|
|
|
+
|
|
|
+## 4. SQL/迁移契约
|
|
|
+
|
|
|
+- **无新表**(复用 `health_data_sources` 读 + `health_dimension_scores` 写)。
|
|
|
+- 唯一迁移变更(若拍板):
|
|
|
+ 1. `health_dimension_scores.data_source` 注释与取值枚举扩展 `DEVICE`(可选,待拍板项 ①);
|
|
|
+ 2. 如需幂等 upsert:`health_dimension_scores` 加唯一索引(member_id, dimension, assess_date)——**⚠ 影响存储**:该表已被报告/手动链路共用,加唯一索引需先查重清理,**列为待拍板项 ④,默认不加**(沿用读最新覆盖)。
|
|
|
+
|
|
|
+## 5. 测试侧证据要求(契约合入后转 TR 补证,防伪证)
|
|
|
+
|
|
|
+| 编号 | 证据 | 载体建议 |
|
|
|
+|---|---|---|
|
|
|
+| E-1 | 桥接单元:mock `health_data_sources` 行 → `health_dimension_scores` 读写正确(含 body_weight 双维度展开、未知 dimension 丢弃、多行 LIFO 覆盖) | JUnit(`HealthDataSourceBridgeServiceTest`) |
|
|
|
+| E-2 | 同步链路桥接:D-3/X-3 同步 mock 数据 → `/api/dimension/overview` 读到对应维度新得分 | MockMvc + 契约 JUnit |
|
|
|
+| E-3 | 档案展示:同步后 `/api/health/indicators/current` 可见 D/X 来源指标 | MockMvc |
|
|
|
+| E-4 | 小程序端点 B-1/B-2:登录 401 / 有 token 正常 / 分页过滤正确 | MockMvc |
|
|
|
+| E-5 | 前端:绑定 → 同步 → 状态卡 三类流(正常/异常/权限)执行证据 | Playwright(Tests 侧) |
|
|
|
+
|
|
|
+## 6. 契约拍板项清单(实现派发前须逐项裁决)
|
|
|
+
|
|
|
+| 项 | 内容 | 建议 |
|
|
|
+|---|---|---|
|
|
|
+| ① | `data_source` 取值:新增 `DEVICE` 还是沿用 `REPORT` | 新增 `DEVICE`(语义清晰,成本=枚举+注释) |
|
|
|
+| ② | `tier` 可信度:D/X 设备采集 = 2 还是 3 | 2(设备自动采集 ≈ AI 分析级,非手动) |
|
|
|
+| ③ | 桥接触发点:同步内事务桥接(A)vs 读时桥接(B) | A(实时、简单) |
|
|
|
+| ④ | `health_dimension_scores` 是否加唯一索引做幂等 upsert | 不加(默认读最新覆盖,避免动共享表) |
|
|
|
+
|
|
|
+> 以上默认建议为研发负责人先行意见,随本契约提交主理人/产品侧拍板;未拍板项按「保守不改」原则执行。
|
|
|
+
|
|
|
+## 7. 状态与后续
|
|
|
+
|
|
|
+- 本契约 = **契约补建阶段产物**,仅登记不实现;契约入库(合入 origin/cfclub)后:测试侧按 §5 补证据 TR 登记 → 主理人拍板 §6 四项 → 桥接实现(P1)入 TDD 池派发(O-14「不纳入 241 基线」口径不变)。
|