---
url: /docs/api-docs/v1/frontend/dual-env.md
---
# Dev / Prd 双库对照 —— 一致性、仿真数据边界、问题处理

> 生成：2026-09-20 · 依据：**生产双库实测**（v2.0.50，各 104 用例，两库均 0 失败）
> 相关：[接线必读](./wiring-guide) · [通用约定 G2 · X-Env](./#g2-请求头) · [取值速查](./values)

***

## 一句话结论

**两个库的 API 契约完全一致** —— 你**不需要**为它们写两套逻辑。
真正的区别在**数据从哪来**：Dev 库的新号拿到**仿真数据**，Prd 库的新号拿到**默认数据模板**
（按四维画像算的示例数据，响应带 `dataSource` 标记）。

**所以两库的新号都不会看到空页面** —— 但 Prd 那份是「示例」，前端**必须**按
[接线必读 · 兜底 #8](./wiring-guide) 标注，否则用户会以为是自己测的。

***

## 一、契约层：完全一致（有实测依据）

这一点是**实测**出来的，不是推测 —— 同一套 104 用例打两个库：

| 维度 | Dev 库 | Prd 库 |
|---|---|---|
| 用例数 | 104 | 104 |
| 失败 | **0** | **0** |
| 字段结构 | 逐条一致 | 同左 |
| 错误码 | 逐条一致 | 同左 |
| 空值（null）语义 | 逐条一致 | 同左 |
| 字段级 L4 覆盖 | 29/30 | 28/30 |

> 两库跑出的报告**字节级几乎相同**（唯一差异是时间戳与各自的数据内容）。

**由此可推**：接口的**形状**由代码决定，与库无关 —— 所以你调试时看到"两个库返回结构一样"是**正常**的，
不要去找并不存在的差异。

***

## 二、数据层：两库新号都有数，但**来源不同**（这是唯一实质区别）

### Dev 库：新号注册自动获得仿真数据

新用户完成 `submitOnboarding`（提交四维：性别/年龄/身高/体重）后，系统会**自动生成 7 天仿真健康数据**：

| 数据 | 内容 |
|---|---|
| 设备健康 | 7 天步数/距离/卡路里/睡眠 + **6 类指标**（HR / REST\_HR / STRESS / SPO2 / VO2MAX / HRV） |
| 设备绑定 | 1 条荣耀绑定（授权中心显示「已连接」） |
| 运动打卡 | 7 天（含今天）→ 首页热量环、跑者统计有数 |
| 餐食 | 21 条（每日 3 餐）→ 热量环的「摄入」非 0 |
| 赛事 | 1 条马拉松报名 → 证书页赛事段有内容 |
| 训练 | 1 个计划 + 12 任务 + 12 提醒 |

**量级按四维反推**（不是固定模板）：`BMR → TDEE → 步数/跑量/配速`，
体重 70kg 与 100kg 的用户拿到**不同**的数据（配速、消耗、步数都不同）。

### Prd 库：没有**仿真**数据，但有**默认数据模板**（2026-09-20 起）

先说仿真为什么不在 Prd —— 原因有二，**都是设计而非缺陷**：

1. **双守卫 fail-closed**：仿真需要 `V2_SIMULATE_ON_ONBOARDING=1` **且** 请求的 schema 是 `dev`
   —— Dev/Prd 是**同一容器、同一进程的两个 schema**，开关对两者同时生效，
   **`schema=='dev'` 才是真正的边界**（见 `src/app/simulation/application/guards.py`）
2. **真实数据由真实设备产生**：Prd 库数据来自厂商云同步（荣耀/Zepp/小米/Suunto）

**但 Prd 新号不会看到空页面** —— 后端按四维画像算出一套 7 天**默认数据模板**垫底
（`vendor='default_template'`，落 device 五表）：

| 维度 | 内容 |
|---|---|
| 设备健康 | 7 天步数/距离/卡路里/睡眠 + **6 类指标**（HR / REST\_HR / STRESS / SPO2 / VO2MAX / HRV） |
| 睡眠汇总 | 7 天（睡眠分期四段之和 = 总时长） |
| 体成分 | 7 天体重 / BMI / 体脂 / 肌肉 / 水分 / 骨量 |

⚠️ **与仿真的三点关键区别**（不要混淆）：

| | Dev 仿真数据 | Prd 默认模板 |
|---|---|---|
| 来源键 | `simulated` | `default_template` |
| **前端能否分辨** | **不能**（无标记） | **能** —— 响应带 `dataSource: "default_template"` |
| 是否绑设备 | **会**写 1 条荣耀绑定（授权中心显示「已连接」） | **不写绑定** —— 「没绑设备」是事实，不假装 |
| 其他域 | 运动打卡 / 餐食 / 赛事 / 训练计划都有 | **只有设备健康五表**（运动打卡/训练计划**仍为空** —— 那是事实类） |

⚠️ **两库都会「降级」** —— 但用的不是同一套数据：Dev 走仿真、Prd 走模板。**前端不用判库**
（[三、前端该怎么处理](#三前端该怎么处理) 第 1 条），只要判 `dataSource` 键有没有。

***

## 三、前端该怎么处理

### 1. 不要写「按库分叉」的逻辑

契约一致 ⇒ 解析代码一份就够。**不要**写 `if (X_ENV === 'Dev') {...} else {...}`。
（App 产线包固定 `X-Env: Prd`，照示例调即与 App 同库。）

### 2. 但要知道「数据覆盖到哪」是两回事

两库新号都有数据（Dev 仿真 / Prd 模板），但**覆盖范围不同**：
Prd 的模板**只填设备健康五表** —— 运动打卡、训练计划、餐食、赛事**在 Prd 仍为空**
（那些是**事实类**数据，后端不编造；「没报名计划」是实话）。
所以同一段渲染代码在 Prd 仍会遇到 `null` —— 这不是 bug，是**契约里就写明的空值语义**
（见[接线必读](./wiring-guide) 与 [取值速查](./values)）。

⚠️ **最要紧的一条**：`withDefaults` 只补 `undefined`、**不补 `null`**。
后端按「无数据源显式返 null，不伪造」设计，所以**前端必须处理 null** ——
详见[接线必读 · 8 条兜底](./wiring-guide)。

### 3. 怎么验证「有数据的页面」

```
X-Env: Dev  →  新号注册即得完整仿真数据 → 所有页面有内容（含运动/训练/餐食/赛事）
X-Env: Prd  →  新号得默认数据模板 → 设备健康页有内容（带 dataSource 标记），
                运动/训练/餐食/赛事仍为空
```

要验**全页面填充**用 Dev；要验**降级提示**（`dataSource` 横幅）用 Prd。

***

## 四、问题处理（看到差异时怎么判、找谁）

| 现象 | 判定 | 处理 |
|---|---|---|
| **两库返回结构不同**（字段多了/少了/类型变了） | 🔴 **算 bug** —— 契约层不该有差异 | 报后端，附两库的原始响应 |
| **两库同一端点错误码不同** | 🔴 **算 bug** | 同上 |
| **Dev 有数据、Prd 没有** | ⚠️ **看是哪个域** —— 设备健康两库都有（Dev 仿真 / Prd 模板）；运动/训练/餐食/赛事只有 Dev 有（事实类不降级） | 想验证有数据的场景就切 Dev |
| **Prd 有数据、Dev 没有** | ✅ **正常**（Dev 库的仿真只对新注册号生成） | Dev 库对老号请手动调 `simulateData` |
| **同一库、同一用户、同一时刻，两次调用结果不同** | 🔴 **算 bug**（除打卡/餐食等本就递增的数据） | 报后端，注明时间与用户 |
| **Dev 库某用户数据与文档里的示例值不同** | ✅ **正常** —— 仿真数据**按四维生成**，因人而异 | 以**结构**为准，别对具体数值写断言 |
| **Dev 库的数据在重跑后变了** | ⚠️ **可能正常** —— 仿真确定性**只在同一版本内成立**（改任何算法细节都会变） | 见下方"确定性的边界" |

### ⚠️ 确定性的边界（重要，避免误报）

仿真数据的"确定性"承诺是：**同一版本内，同一用户 + 同一天 → 逐字节相同**。

它**不跨版本** —— 后端一旦调整生成算法（哪怕只是新增一个指标），其后所有随机值都会改变。
实测：把按日指标从 1 种扩到 6 种后，同一用户的打卡总距离从 32.2km 变成 33.4km。

**所以**：

* ✅ 可以断言**结构**（字段存在、类型、取值范围）
* ❌ **不要**把具体数值写进前端断言或联调用例

> **Prd 的默认数据模板同理** —— 它是**确定性计算**（同一四维 + 同一版本 → 同一份模板，
> 不像仿真那样逐用户随机），但**同样不跨版本**：模板算法一改，数值就变。
> 「同一用户每次刷新数值一样」是**正常**的（这正是模板与仿真的本质区别 —— 模板必须可预测）。

### 排查路径

1. **先确认 `X-Env` 全程一致** —— token 绑 X-Env，跨库即失效（[G2](./#g2-请求头)）
2. 确认是**契约差异**还是**数据差异**（对照上表）
3. 契约差异 → 附「两库原始响应 + 请求头」报后端
4. 数据差异 → 先看是不是仿真范围（本页第二节）
5. 仍不确定 → 用 `POST /api/v1/simulation` 的 `status` action 查当前环境能不能造数据：
   ```json
   { "action": "status", "payload": {} }
   → { "allowed": true, "schema": "dev", "days": 7, "userId": "u000078" }
   ```

***

## 五、仿真数据的操作入口（Dev 库）

| action | 作用 |
|---|---|
| `simulateData` | 按当前画像**重新生成**（幂等：先清后写，不翻倍）。改了画像后重跑可覆盖旧数据 |
| `clearSimulatedData` | 清除该用户的**全部仿真数据**（按 `source`/`vendor` 标记删除，**真实数据不受影响**） |
| `status` | 只读：当前环境能不能造数据（守卫判定） |

统一入口 `POST /api/v1/simulation`（**需登录**，Bearer access）。

⚠️ **不在 Prd 库可用** —— 会返回 `40428003 DISABLED`（这是设计，不是故障）。
