Appearance
Dev / Prd 双库对照 —— 一致性、仿真数据边界、问题处理
生成:2026-09-20 · 依据:生产双库实测(v2.0.50,各 104 用例,两库均 0 失败) 相关:接线必读 · 通用约定 G2 · X-Env · 取值速查
一句话结论
两个库的 API 契约完全一致 —— 你不需要为它们写两套逻辑。 真正的区别在数据从哪来:Dev 库的新号拿到仿真数据,Prd 库的新号拿到默认数据模板 (按四维画像算的示例数据,响应带 dataSource 标记)。
所以两库的新号都不会看到空页面 —— 但 Prd 那份是「示例」,前端必须按 接线必读 · 兜底 #8 标注,否则用户会以为是自己测的。
一、契约层:完全一致(有实测依据)
这一点是实测出来的,不是推测 —— 同一套 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 —— 原因有二,都是设计而非缺陷:
- 双守卫 fail-closed:仿真需要
V2_SIMULATE_ON_ONBOARDING=1且 请求的 schema 是dev—— Dev/Prd 是同一容器、同一进程的两个 schema,开关对两者同时生效,schema=='dev'才是真正的边界(见src/app/simulation/application/guards.py) - 真实数据由真实设备产生: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,是契约里就写明的空值语义 (见接线必读 与 取值速查)。
⚠️ 最要紧的一条:withDefaults 只补 undefined、不补 null。 后端按「无数据源显式返 null,不伪造」设计,所以前端必须处理 null —— 详见接线必读 · 8 条兜底。
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 的默认数据模板同理 —— 它是确定性计算(同一四维 + 同一版本 → 同一份模板, 不像仿真那样逐用户随机),但同样不跨版本:模板算法一改,数值就变。 「同一用户每次刷新数值一样」是正常的(这正是模板与仿真的本质区别 —— 模板必须可预测)。
排查路径
- 先确认
X-Env全程一致 —— token 绑 X-Env,跨库即失效(G2) - 确认是契约差异还是数据差异(对照上表)
- 契约差异 → 附「两库原始响应 + 请求头」报后端
- 数据差异 → 先看是不是仿真范围(本页第二节)
- 仍不确定 → 用
POST /api/v1/simulation的statusaction 查当前环境能不能造数据: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(这是设计,不是故障)。