docs: 归档天工·商品标签(OpenGoods)规划文档

新增 docs/planning/ 规划文档归档:
- 00-final-plan: 最终规划(决策+架构+M0~M5任务清单)
- 01-detailed-design-v2.0: 分类/单位/数据库/API/治理/采集/部署/众包
- 02-advanced-topics-v3.0: OpenAPI/全表DDL/数据契约/OFF映射/中国合规/测试/安全/图片/SLA/竞品
- history/: v0.1~v1.0 演进记录
更新 README 为项目介绍并链接规划文档。

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-06-08 06:05:29 +00:00
parent 729127661e
commit 54175238ad
8 changed files with 1486 additions and 2 deletions
+373
View File
@@ -0,0 +1,373 @@
# 商品档案公益 API 系统 — 深化规划 (v2.0)
> 在 v1.0 定稿基础上,全面展开 8 个方向,并新增 **商品分类体系** 与 **单位管理体系** 两章。
> 不变原则:**只采集 + 只提供信息,绝不涉及购买/交易行为。** 全文仍为规划,未写代码。
**目录**
- A. 商品分类体系(新增)
- B. 单位管理体系(新增)
- 1. 数据库详细设计
- 2. API 详细契约
- 3. 数据治理(去重/冲突/质量评分/溯源)
- 4. 采集合规细则
- 5. 部署与运维
- 6. 众包贡献流程
- 7. 项目治理(域名/许可/免责)
- 8. 时间与里程碑估算
---
## A. 商品分类体系(Taxonomy
商品分类是整个档案库的骨架,直接影响搜索、参数模板、去重。建议**对齐国际标准 + 自建可读品类树**双轨。
### A.1 采用 GS1 GPC 作为标准骨架
GS1 **GPCGlobal Product Classification** 是四层、规则化的全球商品分类,8 位数字编码:
```
Segment(段) → Family(族) → Class(类) → Brick(砖)
47000000 47100000 47101800 10000xxx
清洁/卫生 清洁用品 ... 具体品类(GTIN挂这里)
```
- 全球 44 个 Segment,食品快消主要落在 **Food/Beverage/Tobacco****Cleaning/Hygiene** 等段。
- **Brick** 是最细粒度,商品(GTIN)挂在 brick 上;每个 brick 可带 ≤25 个属性,正好对应我们的"品类参数模板"。
- 好处:与 GS1/电商/数据池天然对齐,便于将来对接 OFF、USDA、GS1-China。
### A.2 三层映射策略
| 层 | 用途 | 来源 |
|----|------|------|
| **标准码 (gpc_brick_code)** | 机器对齐、跨源映射 | GS1 GPC |
| **自建品类树 (category)** | 人类可读、网站导航、中文友好 | 自建,映射到 GPC |
| **来源原始分类 (source_category)** | 保留溯源 | OFF categories / USDA / GS1 |
> OFF 有自己的 categories taxonomy(标签式、多语言),导入时做 `OFF category → 自建 category → GPC brick` 的映射表,未命中的进人工/众包校对队列。
### A.3 品类参数模板(Category Schema
每个叶子品类定义"应有哪些参数",用于:① 数据完整度评分 ② 录入/校验约束 ③ API 返回结构提示。
```jsonc
// category_schema 示例: 包装水
{
"category_id": "beverage/packaged_water",
"gpc_brick_code": "10000159",
"required_attributes": ["net_content", "shelf_life"],
"recommended_attributes": ["ph", "tds", "water_type"],
"nutriment_basis": "per_100ml"
}
```
### A.4 分类落地要点
- 分类树存为**邻接表 + 物化路径**(`path` 列,便于子树查询)。
- 多对一:一个商品归一个主品类(primary),可挂多个辅助标签(labels)。
- 分类可演进:用 `category_version` 管理重命名/合并,旧 ID 重定向不破坏 API。
---
## B. 单位管理体系(Units
食品参数单位混乱(g/kg/ml/L/份/%/kcal/kJ…),必须有统一的**单位字典 + 量纲 + 归一化**机制,否则无法比较和检索。
### B.1 量纲与基准单位
| 量纲 (dimension) | 基准单位 (canonical) | 常见单位 |
|------------------|----------------------|----------|
| 质量 mass | g | mg, g, kg, 斤, oz, lb |
| 体积 volume | ml | ml, L, cl, fl oz |
| 能量 energy | kJ | kJ, kcal(同时存两者) |
| 数量 count | 个 | 个/瓶/包/片/粒 |
| 比例 ratio | %(或无量纲) | %, ‰, mg/100g |
| 长度 length | mm | mm, cm, m, in |
| 时间(保质期) duration | 天 | 天/月/年 |
### B.2 单位字典 `unit`
```jsonc
{
"code": "kg",
"dimension": "mass",
"to_canonical_factor": 1000, // 1 kg = 1000 g
"canonical": "g",
"aliases": ["千克", "公斤", "kgs"],
"display": "kg"
}
```
### B.3 归一化规则
- **入库双存**:原始值/单位 `{value, unit}` + 归一化值 `{canonical_value, canonical_unit}`,原始保留供溯源与展示。
- **营养基准统一**:全部折算到 `per_100g``per_100ml`(按品类模板决定),并保留 `serving_size` 原值。
- **能量双单位**:同时存 kJ + kcal1 kcal ≈ 4.184 kJ),缺一个则自动换算并标记 `derived=true`
- **不可换算**:count(个/瓶)等不跨量纲换算;只做单位别名归一。
- **精度与舍入**:用十进制(`NUMERIC`)避免浮点误差;记录有效数字。
- **冲突处理**:单位无法识别 → 入"待清洗队列",不丢数据。
### B.4 单位与 API
- API 默认返回**原始单位 + 归一化值**两套;可加 `?unit_system=metric|original` 控制展示。
- 搜索/过滤一律基于 canonical 值(如"热量<200kcal/100g")。
---
## 1. 数据库详细设计
PostgreSQL。核心:关系表 + JSONB 灵活属性 + 结构化营养子表。以下为 DDL 草案(写代码时落到 `migrations/`)。
### 1.1 ER 概览
```
brand 1───* product *───1 category ───* category_schema
manufacturer 1───* product
product 1───1 food_detail
product 1───* product_msrp
product 1───* product_source (溯源, 字段级)
product *───* attribute (via product_attribute, 或 JSONB)
unit (字典) attribute_definition (参数字典)
contribution / merge_log / source (治理与登记)
```
### 1.2 关键建表草案(节选)
```sql
CREATE TABLE product (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
gtin VARCHAR(14) UNIQUE, -- 可空(无条码商品)
name TEXT NOT NULL,
brand_id UUID REFERENCES brand(id),
manufacturer_id UUID REFERENCES manufacturer(id),
category_id UUID REFERENCES category(id),
gpc_brick_code VARCHAR(8),
net_content_value NUMERIC,
net_content_unit VARCHAR(16),
net_content_canonical NUMERIC, -- 归一化(g/ml)
country_of_origin VARCHAR(64),
shelf_life_days INT,
storage TEXT,
attributes JSONB DEFAULT '{}', -- 灵活参数
quality_score NUMERIC(4,3) DEFAULT 0,
status VARCHAR(16) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE food_detail (
product_id UUID PRIMARY KEY REFERENCES product(id) ON DELETE CASCADE,
ingredients_text TEXT,
ingredients JSONB, -- [{name,rank}]
allergens TEXT[],
additives TEXT[],
nutriments JSONB, -- 见单位章, 归一到 per_100g/ml
nutrition_basis VARCHAR(16),
serving_size VARCHAR(32),
nutri_score CHAR(1),
labels TEXT[]
);
CREATE TABLE product_msrp (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
amount NUMERIC(12,2) NOT NULL,
currency CHAR(3) NOT NULL, -- ISO 4217
region VARCHAR(8) DEFAULT 'CN',
source_id UUID REFERENCES source(id),
source_url TEXT,
effective_date DATE,
note TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE product_source ( -- 字段级溯源
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
source_id UUID REFERENCES source(id),
url TEXT,
fields TEXT[], -- 该来源贡献了哪些字段
fetched_at TIMESTAMPTZ,
raw JSONB -- 原始快照
);
```
### 1.3 索引策略
- `product.gtin` 唯一索引;`product.name``pg_trgm` GIN(模糊搜索)。
- `product.attributes``food_detail.nutriments`**JSONB GIN** 索引(参数检索)。
- `category.path``ltree` 或前缀索引(子树查询)。
- 全文检索:初期 `tsvector`(name+brand+ingredients) GIN;规模化后迁 OpenSearch。
- 时间列 `updated_at` 索引(增量同步)。
---
## 2. API 详细契约
只读、版本化、统一信封。下面给核心端点的示例。
### 2.1 按条码查询(最常用)
```
GET /api/v1/products/barcode/3017624010701?fields=name,brand,nutriments,msrp
```
```jsonc
{
"data": {
"id": "…", "gtin": "3017624010701",
"name": "示例牌 巧克力榛子酱 400g", "brand": "示例牌",
"category": "食品/酱料/巧克力酱",
"net_content": {"value":400,"unit":"g","canonical":{"value":400,"unit":"g"}},
"food": {
"nutriments": {"energy_kcal":539,"energy_kj":2255,"fat_g":30.9,"sugars_g":56.3,"salt_g":0.107},
"nutrition_basis":"per_100g", "allergens":["坚果","乳","大豆"]
},
"msrp": {"amount":29.90,"currency":"CNY","region":"CN","effective_date":"2026-01-01",
"note":"官方建议零售价, 本站不提供购买"}
},
"meta": {"version":"v1"},
"sources": [{"source":"Open Food Facts","url":"…","fetched_at":"…","license":"ODbL"}]
}
```
### 2.2 搜索
```
GET /api/v1/products/search?q=巧克力&brand=示例牌&category=酱料&allergen_free=花生&page=1&size=20&fields=…
```
返回 `data:[…]` + `meta:{page,size,total,total_pages}`
### 2.3 端点清单 & 错误码
| 端点 | 说明 |
|------|------|
| `GET /products/barcode/{gtin}` | 条码查 |
| `GET /products/{id}` | ID 查 |
| `GET /products/search` | 搜索/过滤/分页 |
| `GET /products/{id}/nutriments` | 仅营养 |
| `GET /products/{id}/msrp` | 仅官方价(含免责) |
| `GET /brands` `GET /categories` | 品牌 / 品类树 |
| `GET /sources/{id}` | 数据来源透明说明 |
| `GET /healthz` `GET /openapi.json` | 健康检查 / 机读文档 |
错误码:`400`(参数错) `404`(未找到, 返回 `{error:{code:"not_found"}}`) `429`(限流, 带 `Retry-After`) `5xx`(服务端)。统一错误信封 `{error:{code,message,request_id}}`
### 2.4 跨切面
- **版本化** `/v1/`;破坏性变更升 `/v2/`,旧版保留过渡期。
- **限流**:匿名 IP 默认 60 req/min(可调);免费 API Key 提配额。响应头 `X-RateLimit-*`
- **缓存**`Cache-Control` + ETagCDN + Redis;条码查命中率高。
- **CORS**:开放 GET(公益 API)。
- **分页**`page/size`(上限 100);大结果集用 `search_after` 游标(OpenSearch 阶段)。
---
## 3. 数据治理
### 3.1 实体去重 / 匹配
1. **强匹配**:相同 `gtin` → 同一商品(条码是天然主键)。
2. **弱匹配**(无 gtin 或 gtin 缺失):`(规范化品牌 + 规范化型号/名称 + 净含量)` 相似度(trigram/编辑距离)+ 阈值;命中候选进**人工/众包确认**,不自动硬合并。
3. **合并**:保留一条 canonical,其余标 `status=merged` 并写 `merge_log`(可回滚)。
### 3.2 多源字段冲突解决
- 每个字段记录来源 + 时间 + 来源可信度权重。
- 冲突时:① 按**来源可信度**(GS1官方 > 厂商官网 > OFF众包 > 第三方)② 同级取**最新** ③ 数值类可取多数/中位数。
- 保留所有来源值于 `product_source.raw`,对外 `sources` 字段透明展示"该字段来自谁"。
### 3.3 质量评分公式(0~1
```
quality_score = 0.4*完整度 + 0.3*来源权威度 + 0.2*多源一致性 + 0.1*新鲜度
完整度 = 命中品类模板 required/recommended 字段的比例
权威度 = 贡献字段的来源权重加权
一致性 = 多源同字段一致的比例
新鲜度 = 最近更新时间衰减
```
低分商品在搜索中降权,并进入"待补全"队列(可派给众包)。
### 3.4 溯源(Provenance
字段级溯源:每条数据可回答"这个营养值/价格来自哪个来源、什么时间、什么许可"。这是公益项目可信度的核心,也用于许可合规标注。
---
## 4. 采集合规细则
### 4.1 通用护栏
- 严格遵守 `robots.txt` 与各源服务条款;礼貌限速(OFF 读 ≤15 req/min/IP);错峰;明确 `User-Agent` 标识本项目身份与联系方式。
- 只采**客观参数**;不照搬受版权保护的营销文案/评测原文(链接来源即可)。
- 增量优先:用 `last_modified`/dump 差异做增量,避免重复抓取。
### 4.2 各源接入步骤
| 源 | 步骤 | 许可 |
|----|------|------|
| **OFF dump**(首批种子) | 下载 `en.openfoodfacts.org.products.csv.gz`(~0.9G压缩) → 解析 → 映射字段 → 入库 | ODbL(衍生库需 ODbL+署名) |
| **OFF API**(增量) | 按 gtin 拉取/按更新时间增量;遵守限速 | 同上 |
| **USDA FoodData Central** | 申请免费 API key;或下载 Branded/Foundation JSON;补全营养 | CC0(最宽松) |
| **GS1 / 中国商品信息服务平台** | 条码→品牌/规格/厂商;API ≤1000 GTIN/次(需授权) | 受限,按授权使用 |
| **厂商官网** | 逐站 adapter,遵守 robots,取官方规格表/MSRP | 取客观参数 |
### 4.3 许可合规
- OFF=ODbL(传染性,衍生数据库须同样开放+署名 OFF);USDA=CC0。
- 对外数据库整体采用 **ODbL + 署名**;每条数据按 `sources[].license` 标注其来源许可,避免冲突。
---
## 5. 部署与运维
### 5.1 演进路径
- **初期**Docker Compose 一键起 `postgres + redis + minio + go-api + python-worker`,单机即可跑通 MVP。
- **成长期**:API 多副本 + 读副本数据库 + CDN;worker 横向扩展。
- **规模化**K8sAPI Deployment + HPA、worker Job/CronJob)、OpenSearch 集群、对象存储用云 S3。
### 5.2 可观测性
- 指标:Prometheus(QPS、延迟、缓存命中、限流计数、采集成功率)。
- 日志:结构化日志 + request_id 贯穿。
- 链路:OpenTelemetryAPI → DB)。
- 告警:错误率/延迟/采集失败/磁盘。
### 5.3 备份与可靠性
- Postgres 每日全量 + WAL 归档;定期恢复演练。
- 对象存储多版本/冗余。
- 采集 worker 幂等 + 重试 + 死信队列。
### 5.4 成本(量级估算,公益项目控成本)
- MVP:单台小型云主机(2C4G)+ 对象存储即可(月成本很低)。
- OFF 食品子集约数百万条,PG 单实例可承载;图片走对象存储 + CDN(按流量)。
- 详细预算待定(取决于云厂商与访问量),可后续出一版成本表。
---
## 6. 众包贡献流程
公益库靠社区补全/纠错。流程:
1. **提交**:用户对某商品提交新增/修改(带可选来源链接、照片)。
2. **校验**:单位/格式/品类模板校验 + 反垃圾(限频、信誉分、验证码)。
3. **审核**:低风险字段自动接受并标 `source=community`;高风险(价格、品牌)进人工/资深用户审核队列。
4. **版本化**:每次修改存历史版本,可 diff、可回滚(类似 wiki)。
5. **信誉系统**:贡献被采纳提升信誉;高信誉用户审核权更大。
6. **溯源透明**:众包数据与官方数据在 `sources` 中明确区分。
> 注意:众包内容也要遵守"只客观信息、不导购",并保留权利方下架通道。
---
## 7. 项目治理(域名/许可/免责)
- **品牌/域名**:建议中性、表意清晰的名字(如 *商品档案 / OpenGoods* 之类),后续选定。
- **代码许可**:开源(如 MIT/Apache-2.0),鼓励复用。
- **数据许可****ODbL + 署名**(因含 OFF);API 文档明示再利用条款。
- **隐私**:不收集个人数据(PII),只处理商品信息;众包账号信息最小化。
- **免责声明(站点显著位置)**
- "本站为公益信息平台,**仅提供商品参数信息,不提供任何购买/交易服务**。"
- "价格为官方建议零售价历史快照,实际售价以零售商为准,**不构成消费或购买建议**。"
- "数据来自多来源并标注出处,可能存在误差;欢迎纠错,权利方可申请更正/下架。"
- **下架/纠错渠道**:公开邮箱/表单,承诺响应时限。
---
## 8. 时间与里程碑估算
> 仅为相对工作量估算(以"理想工作日"计,非承诺排期);实际取决于投入人力与数据源接入难度。
| 里程碑 | 内容 | 估算 | 依赖 | 主要风险 |
|--------|------|------|------|----------|
| **M0 地基** | Go/Python 骨架、Compose、CI、数据契约 | 35 d | — | 低 |
| **M1 数据模型** | 迁移、分类树、单位字典、参数模板 | 4–6 d | M0 | 分类/单位建模需打磨 |
| **M2 种子数据** | OFF dump 导入 + 单位归一 + 分类映射 | 5–8 d | M1 | dump 体量大、字段映射脏 |
| **M3 MVP API(Go)** | 端点 + 缓存/限流 + OpenAPI | 58 d | M1,M2 | 检索性能调优 |
| **M4 采集管线(Python)** | OFF/GS1 adapter + ETL + 去重 + 质量分 + 调度 | 8–12 d | M2 | 去重/冲突算法、合规 |
| **M5 开放/规模化** | 搜索引擎、CDN、API Key、众包后台、文档站 | 1015 d | M3,M4 | 众包审核与防滥用 |
关键路径:M0→M1→M2→M3(最快拿到可查询 MVP);M4/M5 可与后续并行迭代。
---
## 9. 待确认(本版新增点)
1. **分类标准**:认同以 **GS1 GPC** 为标准骨架 + 自建中文品类树映射吗?
2. **单位策略**:营养统一折算到 `per_100g/per_100ml`、能量双存 kJ+kcal,认同吗?
3. **质量评分权重**:上面的 0.4/0.3/0.2/0.1 权重是否合适,或你有偏好?
4. **众包**:第一阶段就要做众包贡献,还是先纯采集、后期再开放众包?
5. **项目命名/域名**:有想好的名字吗?没有的话我可以提几个候选。
> 确认后我把 v2.0 收敛为可执行的工程任务清单。需要动手写代码时你说一声,我从 M0 开始搭骨架开 PR。