Просмотр исходного кода

docs(contract): EVD-17 D/X域契约核对批次(华为/小米运动健康接入)

- 新增 docs/superpowers/specs/2026-11-07-dx-health-contract.md:D1-D5 / X1-X5 端点契约表(实测载体+行号)、数据表契约(huawei/xiaomi_health_tokens + health_data_sources 复用)、鉴权契约(B-50 401)、mock/真实通道语义、三类流建议用例点、契约缺口登记(GAP-A 消费链路 / GAP-B 追加无去重 / GAP-D 表注释陈旧等)
- API_REFERENCE §4.43/4.44 增补 EVD-17 契约登记(与实现核对一致无需修正,登记缺口与双轨复用载体)
- 端点核对:D1-D5 / X1-X5 全部成立(10/10 ✅);⚠a 缺口登记、⚠b 双轨载体成立(复用报告链路)、⚠c 语义=追加写+读最新覆盖
- 无业务代码/测试/DB 迁移改动;mvn -DskipTests compile EXIT=0
E2E Test Bot 5 дней назад
Родитель
Сommit
803f87fc36
2 измененных файлов с 235 добавлено и 0 удалено
  1. 4 0
      docs/superpowers/api/API_REFERENCE.md
  2. 231 0
      docs/superpowers/specs/2026-11-07-dx-health-contract.md

+ 4 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -1468,6 +1468,8 @@ interactionType 取值:`PHONE` / `WECHAT` / `MEETING` / `OTHER`。
 > **数据表**:`huawei_health_tokens`(D1,迁移452);同步入库 `health_data_sources`(sourceType='huawei_health',复用表,未新建)。
 > **数据表**:`huawei_health_tokens`(D1,迁移452);同步入库 `health_data_sources`(sourceType='huawei_health',复用表,未新建)。
 >
 >
 > **通道开关**:`huawei.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实 Health Service Kit 云侧 REST,真实调用处标注 TODO)。
 > **通道开关**:`huawei.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实 Health Service Kit 云侧 REST,真实调用处标注 TODO)。
+>
+> **EVD-17 契约核对(2026-11-07)**:端点/数据表/鉴权均与实现一致;契约文档 `docs/superpowers/specs/2026-11-07-dx-health-contract.md`。已登记契约缺口——① 同步数据只落 `health_data_sources`,未桥接五维 body.* 子维得分(`health_dimension_score`)/健康档案/小程序读取端点(GAP-A);② `health_data_sources` 复用写入为纯追加,无 DB 级去重/覆盖(GAP-B,同步幂等性不保证);③ 手动导入双轨载体复用报告上传/解析/绑定链路 + `/api/family/manual-score/*` + `/api/dimension/upload`(GAP 无,D/X 控制器内无手动导入端点属设计预期)。
 
 
 ---
 ---
 
 
@@ -1490,6 +1492,8 @@ interactionType 取值:`PHONE` / `WECHAT` / `MEETING` / `OTHER`。
 > **通道开关**:`xiaomi.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实云服务接口 hlthopen.io.mi.com,真实调用处标注 TODO)。
 > **通道开关**:`xiaomi.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实云服务接口 hlthopen.io.mi.com,真实调用处标注 TODO)。
 >
 >
 > **合规红线**:仅官方云服务认证鉴权;不引入境外聚合中转(如 Terra),不碰社区逆向接口(userId/passToken 仿冒)。
 > **合规红线**:仅官方云服务认证鉴权;不引入境外聚合中转(如 Terra),不碰社区逆向接口(userId/passToken 仿冒)。
+>
+> **EVD-17 契约核对(2026-11-07)**:端点/数据表/鉴权均与实现一致;契约文档 `docs/superpowers/specs/2026-11-07-dx-health-contract.md`。已登记契约缺口——① 同步数据只落 `health_data_sources`,未桥接五维 body.* 子维得分/健康档案/小程序读取端点(GAP-A);② `health_data_sources` 复用写入为纯追加、无 DB 级去重/覆盖(GAP-B);③ 小米无定时任务(仅 X-3 手动触发,华为有 `HuaweiHealthSyncJob` 03:15)。
 
 
 ---
 ---
 
 

+ 231 - 0
docs/superpowers/specs/2026-11-07-dx-health-contract.md

@@ -0,0 +1,231 @@
+# D/X 域契约文档 · 华为 / 小米运动健康接入(EVD-17)
+
+> **批次归属**:契约补建计划独立批次(O-14 主理人裁决 2026-11-07 17:5x,commit 78277897e)——**D/X 域作 O-7 独立契约核对批次,不纳入 241 SC 基线**(当前 DEV_GOAL=241 无 D/X 域条目,基线与台账不膨胀)。
+> **核对方式**:契约核对 = 文档核对(`docs/2026-11-06-华为运动健康开放平台接入方案.md` v1.2 + `docs/superpowers/api/API_REFERENCE.md` §4.43/§4.44)+ 端点比对(`cfc-backend/src/main/java` 实测定位,防假 RED)。
+> **实现基线**:`4344688d2`(feat xiaomi-health X1-X5)、`978d1bc95`(feat huawei-health D1-D5)、`4815aae95`(fix isBound intValue)、`cc7b3e00b`(B-50 华为 5 端点补鉴权,与 xiaomi 对齐)。
+> **核对日期 / 执行**:2026-11-07 · CFC 后端契约核对会话(EVD-17)
+> **产出后转测试侧(TR-17)**:按 O-7 口径补契约 JUnit + 三类流执行证据。
+
+---
+
+## 1. 核对结论总览
+
+| 项 | 结果 |
+|----|------|
+| D1-D5(华为 `/api/huawei-health/*`)端点载体 | ✅ 5/5 成立(`HuaweiHealthController` 实测定位) |
+| X1-X5(小米 `/api/xiaomi-health/*`)端点载体 | ✅ 5/5 成立(`XiaomiHealthController` 实测定位) |
+| ⚠a 同步数据落库后的前端消费链路契约段 | ❌ **缺口(GAP-A)**:同步只落 `health_data_sources`,未桥接五维 body.* 得分(`health_dimension_score`)/健康档案展示/小程序读取端点 |
+| ⚠b 儿童/全家档案「手动导入」双轨端点载体 | ✅ 载体成立(等效复用,见 §3b):报告上传/解析/绑定链路 + 五维手动填分 + 维度手动录入均存在;D/X 控制器内**无**手动导入端点属设计预期(非缺口) |
+| ⚠c `health_data_sources` 复用写入的校验/去重/覆盖语义 | ✅ 语义确认(见 §3c):**追加写 + 读最新覆盖**;无 DB 级去重/覆盖约束(幂等性不保证) |
+| API_REFERENCE §4.43/§4.44 与实现对齐 | ✅ 一致(端点/数据表/通道/鉴权均与实现相符,无需修正;本节补充契约登记引注) |
+
+---
+
+## 2. 端点契约表(D1-D5 / X1-X5)
+
+### 2.1 D 域 · 华为运动健康(`HuaweiHealthController`,`@RequestMapping("/api/huawei-health")`)
+
+文件:`cfc-backend/src/main/java/com/etotem/cfc/controller/huawei/HuaweiHealthController.java`
+
+| 编号 | 端点 | 实现载体(实测) | 请求要点 | 响应要点 | 核对 |
+|------|------|-------------------|----------|----------|------|
+| D-1 | `POST /api/huawei-health/auth-url` | `authUrl` L54-69 | body: `memberId?`, `scope?`(空=默认 scope) | `data{authUrl, memberId, scope, generateAt}`;mock 下 `authUrl=mock://huawei-health/authorize?...` | ✅ |
+| D-2 | `POST /api/huawei-health/callback` | `callback` L79-101 | body: `code`(必填)+ `memberId?` | `data{memberId, bound, status(ACTIVE/fail), expiresAt}`;失败 `Result.error("code 换 token 失败,请重新授权")` | ✅ |
+| D-3 | `POST /api/huawei-health/sync` | `sync` L111-142 | body: `memberId?` + `dataType?`(空=全量 `syncAll`) | 单类型 `data{memberId, syncAt, dataType, result}`;全量 `data{memberId, syncAt, results[]}`;失败 `Result.error`(不支持类型/未绑定/拉取失败) | ✅ |
+| D-4 | `POST /api/huawei-health/revoke` | `revoke` L152-167 | body: `memberId?` | `data{memberId, revoked}`;无有效授权 `Result.error("撤销失败,未找到有效授权")` | ✅ |
+| D-5 | `POST /api/huawei-health/status` | `status` L177-192 | body: `memberId?` | `data{memberId, bound, channel(mock/real), mock, configured}` | ✅ |
+
+> memberId 语义:请求体优先,回退 `@RequestAttribute("currentMemberId")`(JWT 自动反查登录账号自己);切换视角场景由前端显式传 `memberId` 覆盖。
+> 全部 `@PostMapping` + `Result<T>` 包装 + JWT 拦截(见 §5),无 `@GetMapping/@PutMapping/@DeleteMapping`。
+
+### 2.2 X 域 · 小米运动健康(`XiaomiHealthController`,`@RequestMapping("/api/xiaomi-health")`)
+
+文件:`cfc-backend/src/main/java/com/etotem/cfc/controller/xiaomi/XiaomiHealthController.java`
+
+| 编号 | 端点 | 实现载体(实测) | 请求要点 | 响应要点 | 核对 |
+|------|------|-------------------|----------|----------|------|
+| X-1 | `POST /api/xiaomi-health/auth-url` | `authUrl` L62-85 | body: `memberId?`, `scope?` | `data{memberId, authUrl, mode(mock/real), redirectUri?, state?, scope, message?, generateAt}` | ✅ |
+| X-2 | `POST /api/xiaomi-health/callback` | `callback` L95-119 | body: `code`(必填,拒 "null" 字面量)+ `memberId?` | `data{memberId, bound, status(bound/fail), expiresAt}`;异常拦截返回 `Result.error(...e.getMessage())` | ✅ |
+| X-3 | `POST /api/xiaomi-health/sync` | `sync` L129-160 | body: `memberId?` + `dataType?`(空/"null"=全量) | 同 D-3 结构(`XiaomiSyncResult`);失败 `Result.error` | ✅ |
+| X-4 | `POST /api/xiaomi-health/revoke` | `revoke` L170-185 | body: `memberId?` | `data{memberId, revoked}`;小米侧恒返回 `revoked=true`(无令牌也清除本地,见 §3c/XiaomiAuthService.revoke) | ✅ |
+| X-5 | `POST /api/xiaomi-health/status` | `status` L195-212 | body: `memberId?` | `data{memberId, bound, channel, mock, configured, expiresAt?, scope?}` | ✅ |
+
+> 华为 callback(D-2)与小米 callback(X-2)差异:D-2 失败不带异常明细、token 非空时 `status`=token 实际状态;X-2 失败带异常信息、成功时 `status` 恒为 `"bound"`。
+> 小米无 `status` 列(有效=token 记录存在且未过期,`isAuthorized`),撤销语义为**删除**本地记录;华为有 `status` 列(ACTIVE/REVOKED),撤销语义为**标记** REVOKED(保留行)。
+
+### 2.3 同步引擎(D-3/X-3 支撑件,实测)
+
+| 支撑件 | 文件 | 要点 |
+|--------|------|------|
+| 华为同步接口 | `service/huawei/HealthSyncService` | `sync(memberId,dataType)` / `syncAll(memberId)` |
+| 华为同步实现 | `service/huawei/impl/HealthSyncServiceImpl` | 校验 dataType 合法性 → `isBound`(token ACTIVE)→ `fetchByType` → 取最新点 → `NormCompareEngine` 百分位带归一化 → `BAND_SCORE`(0-100) → 写 `health_data_sources` |
+| 小米同步实现 | `service/xiaomi/impl/XiaomiHealthServiceImpl` | 同构;`xiaomi.health.sync-days=7` 拉取窗口 |
+| 归一化常模 | 两实现内置 `DEFAULT_NORMS` | step[3000..15000]、distance[2..12km]、heart_rate[50..90bpm]、sleep[4.5..8.5h]、body_weight(BMI)[18.5..26];band→score 映射 20/40/65/80/90/50 |
+| 置信度 | 两实现 | 有归一化 0.90;常模缺失(STATUS_PENDING)0.60 |
+| 数据类型 → 五维子维 | `HuaweiHealthDataType` / `XiaomiHealthDataType` | step→`body.exercise`、distance→`body.exercise`、heart_rate→`body.immunity`、sleep→`body.sleep`、body_weight→`body.nutrition,body.growth` |
+
+---
+
+## 3. ⚠ 三项待核实实测结论(开工包标注,逐条回填)
+
+### 3a ⚠ 同步数据落库后的前端消费链路契约段 —— ❌ 缺口(GAP-A)
+
+**实测结论**:
+
+1. **写侧**:华为/小米同步引擎均只写 `health_data_sources`(`HealthDataSourceRecordServiceImpl.saveRecord` = `recordMapper.insert`,追加一条;`parsedResult.normalized.score` 为 0-100 归一化得分)。
+2. **五维 body.* 子维得分读侧**:`DimensionScoreServiceImpl.getHealthDimension()` 读 `health_dimension_score`(`HealthDimensionScoreService.getLatestScores`)→ 暴露于 `POST /api/dimension/overview`、`/api/dimension/radar-data`(已标注 deprecated,前端改走 `/api/dimension-registry/*`)。**该链路不读 `health_data_sources`**。
+3. **健康档案展示读侧**:`POST /api/health/indicators/current` 读 `member_indicator_current`;`POST /api/health/body/detail` 读 `health_indicator`(报告派生)。**均不读 `health_data_sources`**。
+4. **`health_data_sources` 现有消费方**:仅管理端 `DimensionAdminController`(`/api/admin/dimension/data-source/{list,detail,save,delete}`,分页查/详情/增删;`/api/admin/dimension/*` 前缀)。`HealthDataSourceRecordService.getLatest/getRecords`、`Mapper.selectByDimension` **无任何业务调用方**;`DimensionScoreServiceImpl` 注入了 `dataSourceRecordService` 但**无任何方法调用(死注入)**——印证设计意图存在但未接线。
+5. **小程序侧**:cfc-frontend 全仓 **0 处** `huawei-health` / `xiaomi-health` 调用,无前端入口消费同步数据。
+
+**结论登记(仅登记不实现)**:同步数据虽带归一化分数入库,但**未**桥接至五维 body.* 子维得分 / 健康档案展示 / 小程序读取端点;设计 v1.2 第四节图中「⑤ 前端展示」段当前为空窗。登记为契约缺口 GAP-A,测试侧不得伪证「同步 → 前端展示」链路(mock 端到端验证仅能覆盖「授权 → token → 拉取 → 映射 → 入库」)。
+
+### 3b ⚠ 儿童/全家档案「手动导入」双轨端点载体 —— ✅ 成立(等效复用)
+
+**实测结论**:设计 v1.2 第七节双轨——不能登录用户(儿童/家属代管)走手动导入、复用报告解析链路。载体实测均存在(**非 D/X 域控制器内**,属复用):
+
+| 载体 | 端点(实测存在) | 落点 |
+|------|------------------|------|
+| 报告上传/解析/绑定链路 | `POST /api/health/report/upload`、`/report/upload-only`、`/report/proxy-parse`、`/report/proxy-confirm`、`/report/bind`、`/report/auto-create-member`(HealthReportController 实测) | 健康报告 + 全家档案(LangGraph 解析) |
+| 五维手动填分 | `POST /api/family/manual-score/{submit,latest,history}`(ManualDimensionScoreController) | `five_dimension_self_check` |
+| 手动维度评分/问卷 | `POST /api/dimension/upload`、`/api/dimension/questionnaire`(HealthDimensionController) | `health_dimension_score` |
+
+**结论登记**:D/X 控制器(`/api/huawei-health`、`/api/xiaomi-health`)**无**手动导入端点——按设计即如此(手动导入复用报告链路,零新增主体依赖),**不构成缺口**;契约登记复用端点清单供测试侧用例引用。五维能量沙盘对两类来源统一归一化口径的目标见 §3a 的 GAP-A(当前仅 bridge 未完成)。
+
+### 3c ⚠ `health_data_sources` 复用写入的校验/去重/覆盖语义 —— ✅ 确认
+
+**实测结论**:
+
+1. **校验**:入库前在同步引擎层校验——dataType 合法性(`XxxHealthDataType.of`,不支持 → error);授权态(`isBound`:华为 token status=ACTIVE / 小米 `isAuthorized` 存在且未过期,未绑定 → error);拉取数据非空(空 → success + storedCount=0,**不落行**)。持久化层 `saveRecord` 无校验。
+2. **去重**:**无**。`health_data_sources` 表无唯一键(仅普通索引 `idx_member_source(member_id, source_type)`);`saveRecord` 为纯 insert。同一 memberId+dataType 同日/同收集期重复同步会**追加多行**。
+3. **覆盖**:**读侧覆盖语义**——展示侧取最新(`selectLatestBySource`:`ORDER BY created_at DESC LIMIT 1`);写侧不删除旧行(历史行保留供审计)。
+4. **token 表对比**:`huawei_health_tokens` / `xiaomi_health_tokens` 均有 `UNIQUE uk_member(member_id)`——华为 exchangeCode「旧 ACTIVE→REVOKED + 插入新行」,小米 saveToken「存在则 updateById 覆盖」。**token 表幂等,数据表不幂等**。
+
+**结论登记**:契约语义 = **追加写 + 读最新覆盖,DB 级去重/覆盖无约束,同步幂等性不保证**。测试断言应按此语义(重复同步 → 行数增长、latest 读值不变);若产品要求同收集期去重需另提契约变更(唯一键/upsert,本批不实施)。
+
+---
+
+## 4. 数据表契约
+
+### 4.1 `huawei_health_tokens`(迁移452,`DatabaseInitializer` L3458/L3526)
+
+| 列 | 类型 | 说明 |
+|----|------|------|
+| id | BIGINT PK AUTO | |
+| member_id | BIGINT NOT NULL | `family_members.id`(仅能登录用户/成人) |
+| access_token | TEXT NOT NULL | AES/GCM 加密,仅后端持有 |
+| refresh_token | TEXT NOT NULL | AES/GCM 加密 |
+| expires_at | DATETIME NULL | |
+| scope | VARCHAR(255) NULL | 授权范围(逗号分隔数据类型) |
+| status | VARCHAR(16) DEFAULT 'ACTIVE' | ACTIVE=有效 / REVOKED=已撤销 |
+| created_at / updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP / ON UPDATE |
+
+约束:`UNIQUE uk_member(member_id)`。写入语义:exchangeCode 旧 ACTIVE→REVOKED + insert 新 ACTIVE(唯一 ACTIVE 行);refreshToken 原地更新密文与过期时间;revoke 标记 REVOKED(保留行)。
+
+### 4.2 `xiaomi_health_tokens`(迁移453,`DatabaseInitializer` L3461/L3554)
+
+| 列 | 类型 | 说明 |
+|----|------|------|
+| id | BIGINT PK AUTO | |
+| member_id | BIGINT NOT NULL | |
+| access_token | TEXT NOT NULL | AES/CBC 加密(`AesUtil`,mock 固定 IV) |
+| refresh_token | TEXT NULL | AES/CBC 加密,小米可能不下发 |
+| expires_at | DATETIME NULL | 无则视为长期有效 |
+| scope | VARCHAR(255) NULL | |
+| created_at / updated_at | DATETIME | |
+
+约束:`UNIQUE uk_member(member_id)`,**无 status 列**。写入语义:saveToken 单行幂等覆盖(存在 update / 缺失 insert);revoke 删除该成员全部记录。
+
+### 4.3 `health_data_sources`(复用表,非 D/X 新增)
+
+| 列 | 类型 | 说明 |
+|----|------|------|
+| id | BIGINT PK AUTO | |
+| member_id | BIGINT NULL | 家庭成员ID |
+| source_type | VARCHAR(20) NOT NULL | **复用枚举:`huawei_health` / `xiaomi_health`**(表注释仍为旧枚举 VOICE/PHOTO/REPORT/MANUAL,注释陈旧,见 GAP-D) |
+| dimension_codes | VARCHAR(100) | 逗号分隔五维子维(如 `body.exercise,body.sleep`) |
+| parsed_result | TEXT | 原始数据 + 归一化 JSON(含 `normalized.{band,status,percentile,score,normSource}`、value/unit/collectDate/raw) |
+| file_url | VARCHAR(500) | 手动导入场景使用 |
+| confidence | DECIMAL(5,4) | 0.90(正常)/ 0.60(常模缺失) |
+| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP |
+
+索引:`idx_member_source(member_id, source_type)`、`idx_member_id(member_id)`——**均非唯一**。写入:**纯追加**;读取:`getLatest`(created_at DESC LIMIT 1)、`getRecords`、`selectByDimension`(JSON_CONTAINS);业务侧当前唯一消费 = 管理端 `/api/admin/dimension/data-source/*`。
+
+---
+
+## 5. 鉴权契约
+
+- `config/JwtInterceptor.LOGIN_REQUIRED_PATHS`:L115 `/api/xiaomi-health/`、L116 `/api/huawei-health/`(**B-50 修复 `cc7b3e00b` 已合入**——未登录访问任一 D/X 端点返回 401,与 xiaomi 对齐)。
+- 认证头:`Authorization: Bearer {JWT}`;`/api/**` 除 8 个公开路径外全部拦截。
+- memberId 传递:请求体 `memberId` 优先 → `@RequestAttribute("currentMemberId")` 回退(JWT 反查登录账号自己);切换视角由前端显式传 `memberId` 覆盖(`ParamUtils.getLong(params.get("memberId"), currentMemberId)`)。
+- 成员适用口径:token 绑定仅限**能登录用户**(`family_members.userId` 非空);**不能登录用户**(儿童/家属代管)数据走手动导入双轨(§3b)。
+
+---
+
+## 6. mock / 真实通道语义
+
+### 6.1 通道开关(application.yml 实测)
+
+| 通道 | 开关(env 覆盖) | 默认 | 切换 |
+|------|------------------|------|------|
+| 华为 | `huawei.health.mock-enabled`(`HUAWEI_HEALTH_MOCK_ENABLED`) | `true`=mock | 凭证批复后 `false`=真实 Health Service Kit 云侧 REST |
+| 小米 | `xiaomi.health.mock-enabled`(`XIAOMI_HEALTH_MOCK_ENABLED`) | `true`=mock | 凭证批复后 `false`=真实云服务接口 hlthopen.io.mi.com |
+
+### 6.2 mock 通道行为(实测)
+
+- 华为 `HuaweiMockHealthApiClient`(`@ConditionalOnProperty mock-enabled=true` 装配):`auth-url` 返回 `mock://huawei-health/authorize?...`;callback **任意 code** 换 mock token(expires 2h);数据 `mockFetch` 确定性生成(`raw.source="mock"`,体重按身高 1.70m/体重 65kg → BMI≈22.5);revoke 本地标记。
+- 小米 `XiaomiHealthApiClientImpl`(无条件装配,内部按 `mockEnabled` 分支):`auth-url` 返回 `https://xiaomi-mock.local/authorize?memberId=...`(`mode=mock` + message);exchange mock token(expires 30 天);数据 mockFetch 确定性生成(`raw.source="mock"`)。
+- **mock 是全链路唯一可测通道**(授权 → token → 拉取 → 归一化 → 入库),测试证据在 mock 通道采集(X5/D5);生产切 real 前必须凭证联调。
+
+### 6.3 real 通道状态(实测)
+
+- 华为:`HuaweiAuthService` exchange/refresh/revoke 与 `HuaweiRealHealthApiClient` 数据拉取均标注 **TODO**(凭证批复前不真正联调);real 下拉取失败不静默(`isBound` 校验不变)。
+- 小米:`XiaomiAuthService` exchange/refresh/revoke 已实现真实 HTTP(RestTemplate POST token-url / revoke-url);`XiaomiHealthApiClientImpl` real 分支调用 `api-base-url=hlthopen.io.mi.com`(同步窗口 `sync-days=7`,超时 connect 5s / read 15s)。
+- `status` 端点:`channel=mock/real` + `configured`(real 下 clientId/secret 非空)。
+- 定时:**仅华为** `HuaweiHealthSyncJob`(cron `0 15 3 * * ?`,`huawei.health.sync-cron` 可配,遍历 ACTIVE 令牌成员全量 syncAll,单成员失败不阻塞);**小米无定时任务**(仅 X-3 手动触发)。
+- 合规红线(沿用设计 v1.2 §11.4):仅官方云服务认证鉴权;不碰社区逆向接口(userId/passToken 仿冒);不引入境外聚合中转(Terra);token 仅后端持有、加密落库、不落小程序、不进日志。
+
+---
+
+## 7. 三类流建议用例点(转测试侧 TR-17,重点标注)
+
+### 流一:授权绑定流(D-1/D-2/X-1/X-2 + D-5/X-5)
+1. 未登录访问任一 D/X 端点(auth-url/callback/sync/revoke/status)→ **401**(B-50 契约定案)。
+2. mock:`auth-url` → `callback(code)` → `status`:`bound=true`、`channel=mock`、`mock=true`;回调后 `huawei_health_tokens`/`xiaomi_health_tokens` 落库(密文,不由任何响应回传明文)。
+3. 重复 callback(重复授权):华为 → 旧 ACTIVE 行置 REVOKED + 新 ACTIVE 行(唯一 ACTIVE 不变);小米 → 单行覆盖(`uk_member`)。
+4. 空 code / `"null"` code / 空 memberId → 400 badRequest(memberId 缺)+ callback 失败 error。
+
+### 流二:同步落库与消费链路(D-3/X-3,重点:幂等与去重)
+1. `sync(dataType=step)` → `health_data_sources` 新增 1 行:`source_type=huawei_health|xiaomi_health`、`dimension_codes=body.exercise`、`parsedResult.normalized.score ∈ [0,100]`、`confidence=0.90`。
+2. `syncAll` → 依序 5 个类型的 Results 数组;任一失败 → `Result.error("部分数据类型同步失败,详见 results")`。
+3. **幂等/去重断言(⚠c 口径)**:同一天对同一类型重复 sync → `health_data_sources` 行数 +1(追加语义);`status/getLatest` 读取值不变(最新覆盖语义);token 表不增长(幂等覆盖)。测试按此断言,**不可**断言行数不变。
+4. 未绑定成员 sync → error「成员未绑定xx账号,请先获取授权」;不支持 dataType → error「不支持的数据类型」;拉取为空 → success + `storedCount=0`(不落行)。
+5. **GAP-A 提示**:不得伪造「同步 → 五维 body.* 得分/健康档案展示」闭环证据(当前链路缺口见 §3a)。
+
+### 流三:撤销授权合规流(D-4/X-4,合规必须先测)
+1. `revoke` → 华为本地 token ACTIVE→REVOKED(行保留)、`status.bound=false`;小米本地 token 记录清除、`bound=false`。
+2. revoke 后 `sync` → error「成员未绑定…」;revoke 后 `status` → `bound=false`、`configured` 保留。
+3. revoke 后重新授权(callback)→ `bound=true`(幂等覆盖,可再次同步)。
+4. 无有效授权时二次 revoke:华为 → `Result.error("撤销失败,未找到有效授权")`(updated=0);小米 → `revoked=true`(清本地幂等)。
+
+---
+
+## 8. 契约缺口登记(仅登记不实现)
+
+| 编号 | 缺口 | 现状(实测) | 建议承接方 |
+|------|------|--------------|-----------|
+| GAP-A | `health_data_sources`(huawei/xiaomi)→ 五维 body.* 子维得分 / 健康档案 / 小程序读取 的消费链路 | 无桥接(`health_dimension_score` 独立)、`getLatest/getRecords/selectByDimension` 无业务调用方、`DimensionScoreServiceImpl.dataSourceRecordService` 死注入、cfc-frontend 0 调用 | 后续独立批次(契约先行) |
+| GAP-B | `health_data_sources` 同收集期去重/覆盖约束 | 纯 append、表无唯一键、同步幂等性不保证(⚠c) | 需产品确认后提契约变更(唯一键/upsert) |
+| GAP-C | `energy_source_config` 未登记 `huawei_health`/`xiaomi_health` 来源 | 设计 v1.2 第五节为「可选」;能量沙盘当前不按来源归一化 | 可选,随 GAP-A 一并评估 |
+| GAP-D | `health_data_sources.source_type` 表注释仍为旧枚举(VOICE/PHOTO/REPORT/MANUAL) | schema.sql L4020 注释陈旧,与复用枚举不符 | 文档/迁移登记(不动代码) |
+| GAP-E | 华为 real 通道 exchange/refresh/revoke/拉取未实现真实调用 | 均标注 TODO;小米 real 通道已实现 HTTP 骨架 | 凭证批复后的联调批次 |
+
+---
+
+## 9. 参考与验证状态
+
+- 设计文档:`docs/2026-11-06-华为运动健康开放平台接入方案.md`(v1.2,第一/四/五/六/七/十一节)
+- 开工包:`docs/tdd/orchestration/dx-health-kickoff-20261107.md`(O-14,EVD-17)
+- API 参考:`docs/superpowers/api/API_REFERENCE.md` §4.43(华为)/§4.44(小米)——本批核对后**与实现一致,无需修正**;本契约文档为其契约补充登记。
+- 实现载体:`controller/huawei/HuaweiHealthController`、`controller/xiaomi/XiaomiHealthController`、`service/huawei/*`(Auth/HealthSync/DataType/TokenCrypto + `impl/{HuaweiMockHealthApiClient,HuaweiRealHealthApiClient,HealthSyncServiceImpl}`)、`service/xiaomi/*` + `service/XiaomiAuthService` + `service/XiaomiHealthTokenService` + `impl/{XiaomiHealthApiClientImpl,XiaomiHealthServiceImpl,XiaomiHealthTokenServiceImpl}`、`job/HuaweiHealthSyncJob`、`config/JwtInterceptor`、`entity/{Huawei,Xiaomi}HealthToken`、`entity/HealthDataSourceRecord`。
+- 编译验证:`mvn -DskipTests compile` **EXIT=0**(含 B-50 修复 `cc7b3e00b` 合并后的工作树);本批无业务代码改动、无测试文件改动、无 DB 迁移。