Files
goods/docs/planning/01-detailed-design-v2.0.md
T
lixu 54175238ad 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>
2026-06-08 06:05:29 +00:00

374 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 商品档案公益 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。