# 数据使用条款要点（**草案 · 待业务/法务定稿**）

> ⚠️ **这不是法律意见，也不是可直接使用的合同文本。** 本文只做一件事：
> 把「跨商家聚合查询」这个能力在**条款层面必须写清的点**列出来，供业务与法务定稿时逐条过。
> 技术形状见 [`PROTOCOL.md`](./PROTOCOL.md) §8。
>
> **为什么现在就要写**：这个能力的开关不是技术开关，是**合同开关**。
> 技术实现 4-6 周就能有，但如果条款里没写清"商家同意了什么、能撤回什么"，
> 能力上线那天就是风险上线那天。所以条款先行。

---

## 一、必须先分清的两类数据（**整份条款的基石**）

品源库里**没有客户资料** —— 客户姓名/手机号/地址/订单收货人/聊天记录**一律不入库**。
这是产品定位（「我们的库里没有你的客户」），不是本文要重新谈判的东西。

所以本文只处理**第二类**：

| | 个体数据 | 统计级数据 |
| --- | --- | --- |
| 是什么 | 某一家店的某个商品的原始值（价格、库存、销量……） | 多家店同类数据的**聚合结果**（分布、分位、趋势） |
| 谁能看 | **只有该店自己**（及其授权渠道） | 达到门槛后，可由参与者查询 |
| 本文立场 | **不参与任何对外聚合**，不得出现在聚合结果里 | 可参与聚合，但受 §三 的门槛约束 |

**条款必须明确写出"统计级数据不含个体数据、且不可反推"** —— 这是商家敢参与的前提。

---

## 二、商家授权（opt-in）

要写进条款的点：

1. **默认关闭**。统计参与**不是**默认开启的；不表态 = 不参与。
2. **显式同意**。同意必须是**单独的、可指认的动作**（不藏在总协议里一并勾选）。
3. **范围可控**。商家能限定参与聚合的**字段范围**（例如允许价格分布、不允许销量），
   以及**类目范围**。
4. **可随时撤回**。撤回入口在产品内可见，不需要走客服。
5. **可查看贡献**。商家能查看"我参与了哪些聚合、贡献了哪些字段"。

### ✅ 已定口径（2026-09-23 用户拍板）

- **同意粒度 = 按店**。连锁多店时**每家店各自同意**（店长有权）；不按组织一刀切。
  理由：参与是基于"这家店的数据"，不是"这个法人的全部数据"；按店也让连锁可以先拿一家店试。
- **撤回不追溯**：已发布的报告**不追回**，但**下一期起不再纳入**。
  条款把这个时点写死（"下一期"= 下一个聚合计算周期），否则每次都要重新谈。
- **「参与」与「查询权」是两件事**（★ **2026-09-30 按用户拍板 D5 改成这条**，见文末附录）：
  **参与 = 你的数据进入跨店匿名统计池**；**查询权按渠道密钥单独授权**，与是否参与无关。
  ⚠️ 本条此前写的是"**对价 = 参与才有查询权**"（不参与者不得查）——
  那个口径**已被否决**，因为它承诺了一件实现没做的事（查询侧没有、也判定不了"请求方是否参与"：
  `ChannelKey` 连 store 外键都没有），而且按字面实施会让**平台钥/渠道合作方全都不能查**，
  与"让第一个外部消费方接进来"直接冲突。**理由与取舍写在文末附录，不要只读到这里。**

---

## 三、聚合的硬约束（**必须写进条款，且技术上强制**）

1. **最小样本量门槛 `k_min`**：参与某次聚合的**独立店数**低于门槛时，
   **不输出任何数值**，只返回"样本不足"。
   建议 `k_min ≥ 10`。理由（要写进条款的"为什么"）：
   > 某条码只有 2 家店时，"平均值"等于把两家的报价贴出来。
2. **不出极值**：`min`/`max` 在样本量小时同样能反推个体，**一律不出**。
   只出分位/分布类指标。
3. **只看统计，不看个体**：聚合结果**不得**包含任何可定位到单店或可反推的字段。
4. **不改变个体可见性**：参与聚合**不得**让任何店的原始数据对其他人变得更可见。

### ✅ 已定口径（2026-09-23 用户拍板）

- **`k_min = 10`**（参与聚合的**独立店数**下限）。定 5 更宽松但可反推风险上升。
- **分桶不足则合并桶**：按月分桶后某桶样本不足时，**向后合并**到够门槛为止；
  合并不够 → **不出该桶**（不允许"给个数再标个警告"——警告最容易被忽略）。
  实现口径见 `server/core/aggregate.py::guard_payload`（**要么给完整分布，要么一个数值都不给**）。

---

## 四、明确不做的事（**写进条款能大幅降低商家戒心**）

- ❌ 不对外披露"某家店的价格是多少"；
- ❌ 不做**排名披露**（"最便宜的 3 家是……"）；
- ❌ 不把聚合结果卖给**能据此定位到具体店**的第三方；
- ❌ 不因为商家**不参与**聚合而降低其服务质量或可见性；
- ❌ 不使用客户资料（库里没有，此处是明确兜底）。

---

## 五、与现有卖点的关系（**别让新条款侵蚀旧卖点**）

现有对外承诺：「数据归商家、可导出、可删除」。

条款定稿时必须检查**不冲突**：

| 现有承诺 | 加入聚合后是否仍然成立 |
| --- | --- |
| 数据归商家 | ✅ 仍然成立（聚合不改变所有权） |
| 可导出 | ✅ 仍然成立 |
| 可删除 | ⚠️ **要写清**：删除后该店的个体数据不再存在，**但它已贡献进历史聚合结果**的部分无法从已发布报告中"减去"。条款必须如实说明这一点，不能让商家以为是"彻底消失" |
| 商家能拒绝被统计 | ✅ 由 §二 的默认关闭 + 可撤回保证 |

> ⚠️ **「可删除」那条是本文最需要法务过的一处**：删除权与不可逆的聚合结果天然冲突，
> 必须用**明确措辞**管理预期，而不是含糊过去。

---

## 六、落地检查单（技术侧必须同时做到，否则条款是空话）

- [x] **未达门槛时 `sample_ok: false` 且不含任何数值** —— 口径收在
      `core/aggregate.py::guard_payload()`（调用方无法"先给出去再判断"），R-74 已实现并变异检验；
- [x] 聚合结果里**没有**可定位单店的字段（含 `min`/`max`）—— R-89 聚合适配器落地时已钉
      （`dist` 只有 p25/p50/p75，`guard_payload` 的返回值里没有第三种状态）；
- [x] 商家侧有**参与开关**（默认关）+ **撤回**入口 + **我贡献了什么**的查看页 —— R-74 已实现
      （`GET/PATCH /api/v1/store/aggregate` + 商家端「数据参与」页）；
- [x] 撤回后**下一期**起不再纳入（口径已定；撤回留痕：谁、何时改的）；
- [x] 有一条守卫测试：**2 家店 → 必须不出数** —— `core/test_aggregate_optin.py`
      （并且配了一条"达标才给数"的反向测试，避免"门槛恒假"的假绿）。

> **已实现并对外可用（2026-09-24 / R-89 启用）**：聚合本体（`/channel/aggregate`）**已启用**。
> 三条硬约束在实现里强制：**独立店数 < `k_min`（默认 10）→ 一个数值都不出**（`dist: null`，不是「给数值 + 警告」）· **只出 p25/p50/p75，不出 min/max**（极值指向具体那一家）· **只统计显式 opt-in 的店**，范围只许收窄（`sales`/`stock`/`supplier`/`cost` 永不参与）。
> ⚠️ **但「已实现」不等于「有数」**：现在 opt-in 的店是 **0 家**，所以任何查询都会得到
> `sample_ok: false`。**这不是技术问题，是还没有足够多商家同意参与。**
> ⚠️ **2026-09-30 审查修正**：本节原来还写着"现在打开 opt-in **不产生任何对外效果**"
> 与"`501` 里带 `readiness`" —— 这两句**与事实相反**（接口自 R-89 起返回 **200**，
> 打开开关的真实效果是**数据进入跨店匿名统计池**）。已澄清：
> **"没有效果"和"样本不足所以查不出结果"是两件完全不同的事**，前者骗人，后者诚实。
> 就绪度 `readiness` 现在挂在 `GET /api/v1/store/aggregate` 的响应里。
> 兜底测试：`core/test_aggregate_optin.py` 与 `core/test_protocol_admin.py` 现在盯的是
> **"已启用"**（原来它们盯的是"未启用" —— 守卫在替错口径站岗）。
      没有这条测试，"k-匿名"就只是文档里的一句话。

---

## 附：口径已定（2026-09-23）—— 条款文本待法务定稿

1. **`k_min = 10`**；分桶不足则向后合并，仍不足则**不出该桶**。
2. **同意粒度 = 按店**；**撤回不追溯已发布报告**，**下一期起生效**。
3. **「参与」= 你的数据进入跨店匿名统计池；查询权按渠道密钥单独授权。**
   > ✅ **2026-09-30 按用户拍板（D5）改成这条口径**，理由是原口径与实现不符：
   > 原文写的是「**参与聚合 = 查询他人聚合结果的前提**（参与才有查询权）」，
   > 而实现里**没有查询侧校验** —— `ChannelKey` 连 store 外键都没有，
   > 请求方"是否参与"在数据模型上判定不了（实现只做了**贡献侧**过滤：
   > 只有 opt-in 的店进池）。所以"未参与者"照样能拿到他人聚合结果。
   >
   > **两条路，以及为什么选改条款**：
   > **(a)** 改实现（给 `ChannelKey` 补店主关联 + 查询侧校验）——
   > 要动模型 + 迁移 + 管理端签发流程，而且**平台钥没有店主**，得单独定规则；
   > 更关键：按字面实施会让**渠道合作方全都不能查**，
   > 与"让第一个外部消费方接进来"直接冲突。
   > **(b)** 改条款措辞（**已选**）—— 最省，且**语义更准**：
   > 「参与」说的是**数据进不进池**，「查询权」是**渠道密钥授权**的事。
   > 这两件事本来就没有关系（**贡献者 ≠ 消费者**），原句把它们绑在一起才是错的。
   >
   > 守卫：`core/test_legal_pages.py::test_六个要素都在` **两头都钉** ——
   > 新口径必须在，旧那句**不许回来**（它承诺了一件实现没做的事）。

**技术侧对应**：`server/core/aggregate.py`（门槛与范围的唯一实现）·
`GET/PATCH /api/v1/store/aggregate`（合同开关）· 商家端「数据参与」页 ·
`/channel/aggregate`（**200**，样本不足时 `sample_ok: false`；就绪度见 `GET /api/v1/store/aggregate`）。
