Compare commits
1 Commits
main
...
planning-docs
| Author | SHA1 | Date | |
|---|---|---|---|
| 54175238ad |
@@ -1,2 +1,24 @@
|
||||
# goods
|
||||
商品档案公开API
|
||||
# 天工·商品标签 (OpenGoods)
|
||||
|
||||
商品档案公开 API —— 公益网站/服务:**采集全网商品信息,对外提供商品参数查询 API**。
|
||||
|
||||
> 核心原则:**只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。**
|
||||
|
||||
## 这是什么
|
||||
|
||||
- 开放、中立、可溯源的「商品参数百科 + 开放 API」
|
||||
- 首批聚焦 **食品快消**,对外提供按条码/名称查询商品参数(成分、营养、规格、官方建议零售价等)
|
||||
- 技术栈:**Go**(对外只读 API) + **Python**(采集/ETL),经 PostgreSQL + Redis/队列解耦
|
||||
|
||||
## 项目状态
|
||||
|
||||
规划阶段。完整方案见 [`docs/planning/`](./docs/planning/README.md):
|
||||
|
||||
- [最终规划](./docs/planning/00-final-plan.md)(决策 + 架构 + 任务清单)
|
||||
- [详细设计 v2.0](./docs/planning/01-detailed-design-v2.0.md)(分类/单位/数据库/API/治理/采集/部署)
|
||||
- [进阶专题 v3.0](./docs/planning/02-advanced-topics-v3.0.md)(OpenAPI/DDL/合规/测试/安全/SLA/竞品)
|
||||
|
||||
## 许可
|
||||
|
||||
- 代码:拟用 Apache-2.0 / MIT(待定)
|
||||
- 数据:拟用 **ODbL + 署名**(因采用 Open Food Facts 等开放数据源)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 天工·商品标签 (OpenGoods) — 最终规划 (Final)
|
||||
|
||||
> 公益网站/服务:采集全网商品信息,提供商品参数查询 API。
|
||||
> **核心原则:只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。**
|
||||
> 本文档为前几版(v0.1 → v2.0)的最终收敛版,所有关键决策已锁定。详细设计见 v2.0 附件。
|
||||
|
||||
---
|
||||
|
||||
## 0. 项目标识
|
||||
- **中文名**:天工·商品标签(呼应《天工开物》)
|
||||
- **英文名**:OpenGoods
|
||||
- **定位**:开放、中立、可溯源的"商品参数百科 + 开放 API"
|
||||
|
||||
---
|
||||
|
||||
## 1. 已锁定的全部决策
|
||||
|
||||
| 维度 | 决策 |
|
||||
|------|------|
|
||||
| 首批品类 | **食品快消** |
|
||||
| 价格 | 只收 **官方标准零售价 (MSRP)**:静态字段,带 currency/region/source/effective_date + 免责;**不收实时电商价、无购买入口** |
|
||||
| 技术栈 | **Go**(对外只读 API/核心服务) + **Python**(采集/ETL/爬虫),经 **PostgreSQL + Redis/队列** 解耦 |
|
||||
| 种子数据 | **Open Food Facts 食品 dump** 先导入,最快有真实数据 |
|
||||
| 数据许可 | 对外数据库用 **ODbL + 署名**;CC0 来源(USDA)自由混入;每条数据按来源标注许可 |
|
||||
| 商品分类 | **GS1 GPC 四层标准码 (Segment→Family→Class→Brick)** 为骨架 + **自建中文品类树** 映射 + 保留来源原始分类 |
|
||||
| 单位管理 | 量纲字典;**原始值 + 归一化值双存**;营养统一折算到 `per_100g/per_100ml`;能量 **双存 kJ+kcal**;用十进制(NUMERIC)防误差 |
|
||||
| 质量评分 | `0.4*完整度 + 0.3*来源权威 + 0.2*多源一致 + 0.1*新鲜度` |
|
||||
| 众包 | **一期不做众包,先纯采集**;二期再开放贡献/纠错(带审核与版本化) |
|
||||
| Go 框架 | `chi` + 标准库 `net/http`(轻量) |
|
||||
| 迁移工具 | `golang-migrate`(纯 SQL,两端共享 schema) |
|
||||
| 部署 | 初期 Docker Compose(postgres+redis+minio+go-api+python-worker)→ 后期 K8s |
|
||||
| 地域 | 先用 OFF 全球食品库起步,后接 GS1-China 补强中国数据 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构(定稿)
|
||||
|
||||
```
|
||||
数据源: OFF dump / OFF API / USDA(CC0) / GS1-China
|
||||
│
|
||||
▼ Python: 采集 adapters → ETL(清洗/单位归一/分类映射/去重/质量评分)
|
||||
│ 写入
|
||||
┌────▼─────────┐ 图片 ┌──────────┐
|
||||
│ PostgreSQL │◀───────▶│ MinIO/S3 │
|
||||
│ (商品档案主库)│ └──────────┘
|
||||
└────▲─────────┘
|
||||
│ 只读 (+Redis 缓存/限流)
|
||||
▼ Go: 公开 REST API + OpenAPI 文档
|
||||
各种软件 / 开发者 (无任何交易端点)
|
||||
```
|
||||
两端不直接互调,通过共享 PostgreSQL schema + 《数据契约文档》协作。
|
||||
|
||||
---
|
||||
|
||||
## 3. 仓库结构(写代码时落地)
|
||||
```
|
||||
goods/ (OpenGoods 天工·商品标签)
|
||||
├── README.md
|
||||
├── LICENSE # 代码: Apache-2.0/MIT; 数据: ODbL 说明
|
||||
├── docker-compose.yml
|
||||
├── docs/{data-contract.md, openapi.yaml, disclaimer.md}
|
||||
├── migrations/ # golang-migrate 共享 SQL
|
||||
├── api/ # Go 只读 API (chi)
|
||||
│ ├── cmd/server/main.go
|
||||
│ └── internal/{handler,store,model,middleware}/
|
||||
└── ingestion/ # Python 采集 + ETL
|
||||
├── adapters/{openfoodfacts,usda,gs1}.py
|
||||
├── etl/{normalize_units,map_category,dedup,quality}.py
|
||||
└── jobs/{seed_off_dump,scheduler}.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 最终可执行任务清单(按里程碑)
|
||||
|
||||
**M0 — 工程地基**(~3–5d)
|
||||
- [ ] Go module + Python 项目骨架
|
||||
- [ ] docker-compose(postgres+redis+minio)
|
||||
- [ ] CI(Go build/vet/test;Python ruff/pytest)
|
||||
- [ ] `docs/data-contract.md`、`docs/disclaimer.md`(不提供购买声明)初版
|
||||
|
||||
**M1 — 数据模型 + 分类 + 单位**(~4–6d)
|
||||
- [ ] migrations:product / food_detail / product_msrp / product_source / brand / manufacturer / category / category_schema / unit / attribute_definition / merge_log
|
||||
- [ ] 导入 GS1 GPC 骨架 + 建自建中文品类树 + 映射表
|
||||
- [ ] 单位字典 + 归一化规则
|
||||
- [ ] 索引:gtin 唯一、name trigram、JSONB GIN、category ltree
|
||||
|
||||
**M2 — 种子数据 (Python)**(~5–8d)
|
||||
- [ ] 下载 OFF 食品 dump → 字段映射(成分/营养/过敏原/图片)
|
||||
- [ ] 单位归一 + 分类映射入库
|
||||
- [ ] USDA(CC0) 营养补全(可选)
|
||||
|
||||
**M3 — MVP API (Go)**(~5–8d)
|
||||
- [ ] 端点:barcode / id / search / nutriments / msrp / brands / categories / sources / healthz
|
||||
- [ ] 统一响应信封、分页、`fields=` 裁剪、错误码
|
||||
- [ ] Redis 缓存 + IP 限流 + OpenAPI 文档
|
||||
|
||||
**M4 — 采集管线 (Python)**(~8–12d)
|
||||
- [ ] adapter:OFF API 增量 + GS1 条码补全
|
||||
- [ ] ETL:清洗/归一/去重合并/冲突解决/质量评分/字段级溯源
|
||||
- [ ] 调度(定时增量更新)
|
||||
|
||||
**M5 — 开放与规模化**(~10–15d)
|
||||
- [ ] 搜索引擎(PG 全文 → OpenSearch)、CDN
|
||||
- [ ] 免费 API Key(防滥用+统计)
|
||||
- [ ] 众包贡献后台(提交/审核/版本/信誉)
|
||||
- [ ] 开发者文档站 + 开放数据许可与免责声明上线
|
||||
|
||||
> 关键路径:M0→M1→M2→M3(最快拿到可查询 MVP);M4/M5 后续并行迭代。
|
||||
|
||||
---
|
||||
|
||||
## 5. 下一步
|
||||
规划已全部定稿。你之前说"先不写代码",所以我**停在这里待命**。
|
||||
等你说"开始",我从 **M0 工程地基** 动手,搭好骨架后开 PR 给你看(也可指定先只做某几个里程碑,例如 M0+M1)。
|
||||
@@ -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 **GPC(Global 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 + kcal(1 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` + ETag;CDN + 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 横向扩展。
|
||||
- **规模化**:K8s(API Deployment + HPA、worker Job/CronJob)、OpenSearch 集群、对象存储用云 S3。
|
||||
|
||||
### 5.2 可观测性
|
||||
- 指标:Prometheus(QPS、延迟、缓存命中、限流计数、采集成功率)。
|
||||
- 日志:结构化日志 + request_id 贯穿。
|
||||
- 链路:OpenTelemetry(API → 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、数据契约 | 3–5 d | — | 低 |
|
||||
| **M1 数据模型** | 迁移、分类树、单位字典、参数模板 | 4–6 d | M0 | 分类/单位建模需打磨 |
|
||||
| **M2 种子数据** | OFF dump 导入 + 单位归一 + 分类映射 | 5–8 d | M1 | dump 体量大、字段映射脏 |
|
||||
| **M3 MVP API(Go)** | 端点 + 缓存/限流 + OpenAPI | 5–8 d | M1,M2 | 检索性能调优 |
|
||||
| **M4 采集管线(Python)** | OFF/GS1 adapter + ETL + 去重 + 质量分 + 调度 | 8–12 d | M2 | 去重/冲突算法、合规 |
|
||||
| **M5 开放/规模化** | 搜索引擎、CDN、API Key、众包后台、文档站 | 10–15 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。
|
||||
@@ -0,0 +1,408 @@
|
||||
# 天工·商品标签 (OpenGoods) — 深化规划 (v3.0)
|
||||
|
||||
> 在最终版基础上,展开全部 10 个进阶方向。仍为规划,未写代码。
|
||||
> 原则不变:**只采集 + 只提供信息,绝不涉及购买/交易。**
|
||||
|
||||
**目录**
|
||||
1. 完整 OpenAPI 规范草案
|
||||
2. 完整数据库 DDL
|
||||
3. 数据契约文档
|
||||
4. OFF 字段映射表
|
||||
5. 中国合规专项
|
||||
6. 测试与数据质量保障
|
||||
7. 安全与反滥用
|
||||
8. 商品图片处理
|
||||
9. 可用性与 SLA
|
||||
10. 竞品 / 同类项目分析
|
||||
|
||||
---
|
||||
|
||||
## 1. 完整 OpenAPI 规范草案(节选骨架,写代码时落到 `docs/openapi.yaml`)
|
||||
|
||||
```yaml
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OpenGoods API (天工·商品标签)
|
||||
version: "1.0.0"
|
||||
description: >
|
||||
公益商品参数查询 API。只提供信息,不提供购买/交易。
|
||||
数据采用 ODbL 许可并署名来源。
|
||||
license: {name: ODbL-1.0, url: https://opendatacommons.org/licenses/odbl/}
|
||||
servers:
|
||||
- {url: https://api.opengoods.org/api/v1}
|
||||
paths:
|
||||
/products/barcode/{gtin}:
|
||||
get:
|
||||
summary: 按条码查询商品档案
|
||||
parameters:
|
||||
- {name: gtin, in: path, required: true, schema: {type: string, pattern: '^[0-9]{8,14}$'}}
|
||||
- {name: fields, in: query, schema: {type: string}, description: 逗号分隔字段裁剪}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/ProductEnvelope'}}}}
|
||||
'404': {description: 未找到, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
|
||||
'429': {description: 限流, headers: {Retry-After: {schema: {type: integer}}}}
|
||||
/products/{id}:
|
||||
get: { summary: 按ID查询, parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}], responses: {'200': {description: OK}} }
|
||||
/products/search:
|
||||
get:
|
||||
summary: 搜索/过滤/分页
|
||||
parameters:
|
||||
- {name: q, in: query, schema: {type: string}}
|
||||
- {name: brand, in: query, schema: {type: string}}
|
||||
- {name: category, in: query, schema: {type: string}}
|
||||
- {name: allergen_free, in: query, schema: {type: string}}
|
||||
- {name: page, in: query, schema: {type: integer, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, default: 20, maximum: 100}}
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/SearchEnvelope'}}}}}
|
||||
/products/{id}/nutriments: {get: {summary: 仅营养}}
|
||||
/products/{id}/msrp: {get: {summary: 仅官方零售价(含免责)}}
|
||||
/brands: {get: {summary: 品牌列表}}
|
||||
/categories: {get: {summary: 品类树}}
|
||||
/sources/{id}:{get: {summary: 数据来源透明说明}}
|
||||
/healthz: {get: {summary: 健康检查}}
|
||||
components:
|
||||
schemas:
|
||||
ProductEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/Product'}
|
||||
meta: {type: object}
|
||||
sources: {type: array, items: {$ref: '#/components/schemas/SourceRef'}}
|
||||
Product:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string, format: uuid}
|
||||
gtin: {type: string}
|
||||
name: {type: string}
|
||||
brand: {type: string}
|
||||
category: {type: string}
|
||||
net_content: {$ref: '#/components/schemas/Quantity'}
|
||||
food: {$ref: '#/components/schemas/FoodDetail'}
|
||||
msrp: {$ref: '#/components/schemas/Msrp'}
|
||||
quality_score: {type: number}
|
||||
Quantity:
|
||||
type: object
|
||||
properties: {value: {type: number}, unit: {type: string}, canonical: {type: object}}
|
||||
FoodDetail:
|
||||
type: object
|
||||
properties:
|
||||
ingredients_text: {type: string}
|
||||
allergens: {type: array, items: {type: string}}
|
||||
additives: {type: array, items: {type: string}}
|
||||
nutriments: {type: object}
|
||||
nutrition_basis: {type: string, enum: [per_100g, per_100ml, per_serving]}
|
||||
nutri_score: {type: string}
|
||||
Msrp:
|
||||
type: object
|
||||
properties:
|
||||
amount: {type: number}
|
||||
currency: {type: string}
|
||||
region: {type: string}
|
||||
effective_date: {type: string, format: date}
|
||||
note: {type: string, default: "官方建议零售价, 本站不提供购买"}
|
||||
SourceRef:
|
||||
type: object
|
||||
properties: {source: {type: string}, url: {type: string}, fetched_at: {type: string}, license: {type: string}}
|
||||
Error:
|
||||
type: object
|
||||
properties: {error: {type: object, properties: {code: {type: string}, message: {type: string}, request_id: {type: string}}}}
|
||||
```
|
||||
|
||||
> 该 `openapi.yaml` 既是契约也是文档源:Go 端用它做路由校验/生成 Swagger UI,客户端可由它生成 SDK。
|
||||
|
||||
---
|
||||
|
||||
## 2. 完整数据库 DDL(全部表)
|
||||
|
||||
```sql
|
||||
-- 扩展
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS ltree;
|
||||
-- gen_random_uuid() 由 pgcrypto 提供
|
||||
|
||||
CREATE TABLE source (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL, -- Open Food Facts / USDA / GS1-China ...
|
||||
homepage TEXT,
|
||||
license TEXT, -- ODbL / CC0 / proprietary
|
||||
trust_weight NUMERIC(3,2) DEFAULT 0.5, -- 来源可信度(冲突解决用)
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE brand (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
normalized_name TEXT, -- 规范化(去空格/大小写/全半角)用于匹配
|
||||
aliases TEXT[],
|
||||
UNIQUE(normalized_name)
|
||||
);
|
||||
|
||||
CREATE TABLE manufacturer (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
normalized_name TEXT,
|
||||
country VARCHAR(64),
|
||||
UNIQUE(normalized_name)
|
||||
);
|
||||
|
||||
CREATE TABLE category (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name_zh TEXT NOT NULL,
|
||||
name_en TEXT,
|
||||
parent_id UUID REFERENCES category(id),
|
||||
path LTREE, -- 物化路径, 子树查询
|
||||
gpc_brick_code VARCHAR(8), -- 映射到 GS1 GPC
|
||||
level INT,
|
||||
UNIQUE(path)
|
||||
);
|
||||
|
||||
CREATE TABLE category_schema ( -- 品类参数模板
|
||||
category_id UUID PRIMARY KEY REFERENCES category(id),
|
||||
required_attributes TEXT[],
|
||||
recommended_attributes TEXT[],
|
||||
nutriment_basis VARCHAR(16)
|
||||
);
|
||||
|
||||
CREATE TABLE unit ( -- 单位字典
|
||||
code VARCHAR(16) PRIMARY KEY,
|
||||
dimension VARCHAR(16) NOT NULL, -- mass/volume/energy/count/ratio/length/duration
|
||||
canonical VARCHAR(16) NOT NULL,
|
||||
to_canonical_factor NUMERIC, -- code -> canonical 的换算因子
|
||||
aliases TEXT[],
|
||||
display TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE attribute_definition ( -- 参数字典(标准名/别名/单位)
|
||||
key VARCHAR(64) PRIMARY KEY,
|
||||
label_zh TEXT, label_en TEXT,
|
||||
dimension VARCHAR(16),
|
||||
default_unit VARCHAR(16) REFERENCES unit(code),
|
||||
aliases TEXT[]
|
||||
);
|
||||
|
||||
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,
|
||||
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', -- active/merged/deprecated
|
||||
canonical_id UUID REFERENCES product(id), -- 被合并到哪个
|
||||
search_tsv TSVECTOR,
|
||||
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,
|
||||
allergens TEXT[],
|
||||
additives TEXT[],
|
||||
nutriments JSONB,
|
||||
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,
|
||||
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_image (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
|
||||
url TEXT, -- 对象存储 URL
|
||||
kind VARCHAR(16), -- front/ingredients/nutrition
|
||||
license TEXT,
|
||||
source_id UUID REFERENCES source(id)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
CREATE TABLE merge_log ( -- 合并/回滚
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kept_id UUID, merged_id UUID,
|
||||
reason TEXT, by TEXT, created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE UNIQUE INDEX idx_product_gtin ON product(gtin) WHERE gtin IS NOT NULL;
|
||||
CREATE INDEX idx_product_name_trgm ON product USING gin (name gin_trgm_ops);
|
||||
CREATE INDEX idx_product_attrs ON product USING gin (attributes);
|
||||
CREATE INDEX idx_food_nutriments ON food_detail USING gin (nutriments);
|
||||
CREATE INDEX idx_product_tsv ON product USING gin (search_tsv);
|
||||
CREATE INDEX idx_category_path ON category USING gist (path);
|
||||
CREATE INDEX idx_product_updated ON product(updated_at);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据契约文档(`docs/data-contract.md` 概要)
|
||||
|
||||
两端共享的"事实约定",避免 Go/Python 理解不一致:
|
||||
- **枚举固定**:`status`(active/merged/deprecated)、`nutrition_basis`(per_100g/per_100ml/per_serving)、`dimension`、`source.license`、`image.kind`。
|
||||
- **字段含义与可空性**:逐字段写明(如 `gtin` 可空、唯一;`quality_score` ∈ [0,1])。
|
||||
- **单位规则**:原始 + canonical 双存;能量双存 kJ/kcal;换算因子来自 `unit` 表。
|
||||
- **写入责任**:仅 Python(ingestion) 写库;Go 只读。所有写入走 ETL,保证归一化与溯源。
|
||||
- **版本**:契约本身版本化;schema 变更需同步更新契约 + 迁移 + OpenAPI。
|
||||
- **示例**:附 1 条完整 product JSON 作为"黄金样例",两端测试都对它断言。
|
||||
|
||||
---
|
||||
|
||||
## 4. OFF(Open Food Facts)字段映射表(导入直接用)
|
||||
|
||||
| OFF 字段 (CSV) | OpenGoods 字段 | 处理 |
|
||||
|----------------|----------------|------|
|
||||
| `code` | `product.gtin` | 校验 8/12/13/14 位 + 校验位 |
|
||||
| `product_name` / `product_name_zh` | `product.name` | 优先中文, 回退英文 |
|
||||
| `brands` | `brand.name` | 拆分取首个, 规范化, upsert brand |
|
||||
| `categories` / `categories_tags` | `source_category` → `category` | 走 OFF→自建→GPC 映射表 |
|
||||
| `quantity` | `net_content_*` | 解析数值+单位 → 归一化 |
|
||||
| `countries` | `country_of_origin` | 取销售国/产地 |
|
||||
| `ingredients_text` | `food_detail.ingredients_text` | 原文保留 |
|
||||
| `allergens_tags` | `food_detail.allergens` | 标签清洗为中文 |
|
||||
| `additives_tags` | `food_detail.additives` | E-number 解析 |
|
||||
| `energy-kj_100g` / `energy-kcal_100g` | `nutriments.energy_kj/kcal` | 缺一个则换算, 标 derived |
|
||||
| `fat_100g` `saturated-fat_100g` `carbohydrates_100g` `sugars_100g` `proteins_100g` `salt_100g` | `nutriments.*` | 归一 per_100g |
|
||||
| `nutriscore_grade` | `food_detail.nutri_score` | A–E |
|
||||
| `serving_size` | `food_detail.serving_size` | 原值 |
|
||||
| `image_url` / `image_front_url` 等 | `product_image.url` | 下载转存对象存储, 记 CC-BY-SA |
|
||||
| `last_modified_t` | `product_source.fetched_at` | 增量基准 |
|
||||
| (整行) | `product_source.raw` | 存原始快照 |
|
||||
|
||||
> 许可:OFF 数据=ODbL(衍生库需同样开放+署名);图片=CC-BY-SA。映射时全程记 `source_id=OFF`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 中国合规专项(公益网站落地关键)
|
||||
|
||||
> 以下为工程与运营层面的合规要点梳理,**非法律意见**;正式上线前建议咨询专业法务。
|
||||
|
||||
### 5.1 网站备案
|
||||
- 服务器在中国大陆 → 需 **ICP 备案**(公益网站可走非经营性 ICP 备案);部分地区/类目可能涉 **公安联网备案**。
|
||||
- 若用境外/港澳服务器可免 ICP,但访问速度与合规另作权衡。
|
||||
|
||||
### 5.2 数据合规(网络安全法 / 数据安全法 / 个人信息保护法)
|
||||
- 本项目**只处理商品信息、不收集个人信息(PII)**,PIPL 风险低;众包阶段涉及用户账号时再做最小化收集 + 隐私政策。
|
||||
- 《数据安全法》要求数据收集合法正当;做好数据分级与安全保护义务。
|
||||
|
||||
### 5.3 网络爬虫法律边界(重点)
|
||||
依据中央网信办公开文章与司法实践,判断标准是**客观结果**——是否妨碍目标网站正常运行 / 危害合法权益:
|
||||
- **守 robots.txt**、礼貌限速、错峰,**不得对目标站造成 DDoS 式压力**(否则可能触及破坏计算机信息系统罪等)。
|
||||
- **不抓取非公开/需登录/绕过反爬**的数据(可能涉非法获取计算机信息系统数据罪)。
|
||||
- 只采**客观公开的商品参数**;不抓取受版权保护内容、不抓个人信息。
|
||||
- 优先用**官方开放数据/API/数据 dump**(OFF dump、USDA、GS1 授权)——从源头规避爬虫风险。
|
||||
|
||||
### 5.4 食品信息合规
|
||||
- 展示食品参数时注明"信息仅供参考,以实物标签为准";营养/成分以官方/厂商标签为准。
|
||||
- 不做医疗/功效宣称;不构成消费建议。
|
||||
|
||||
### 5.5 价格与"不导购"
|
||||
- 价格仅为**官方建议零售价历史快照**,显著标注;**全站无购买/下单/跳转购买链接**,避免被认定为经营性电商导购。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试与数据质量保障
|
||||
|
||||
### 6.1 代码测试
|
||||
- **Go**:handler 单元测试 + store 层用 `testcontainers`/临时 PG 集成测试 + API 契约测试(对 openapi.yaml 校验响应)。
|
||||
- **Python**:ETL 纯函数单测(单位归一、分类映射、去重打分)+ adapter 用录制的样例数据测试(不打真实站点)。
|
||||
- **CI**:PR 必跑 lint + test;覆盖率门槛(如 ETL 核心 ≥80%)。
|
||||
|
||||
### 6.2 数据质量
|
||||
- **入库校验**:gtin 校验位、单位可识别、营养数值合理区间、必填字段(按品类模板)。
|
||||
- **质量评分**:见 v2.0 公式,低分进"待补全"队列。
|
||||
- **数据回归**:黄金样例集 + 定期跑"数据健康检查"(孤儿记录、单位异常、重复 gtin、营养越界)。
|
||||
- **可观测**:导入报表(新增/更新/拒绝条数、拒绝原因 top)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全与反滥用
|
||||
|
||||
- **API 防刷**:IP 限流 + 可选 API Key 分级配额;异常流量识别(突发高频降级/挑战)。
|
||||
- **缓存挡压**:热点条码走 CDN/Redis,降低数据库压力,也抗刷。
|
||||
- **输入校验**:所有参数严格校验(gtin 正则、size 上限),防注入(参数化查询,禁拼 SQL)。
|
||||
- **密钥管理**:DB/对象存储/第三方 key 走环境变量/密钥管理,不入库不入仓。
|
||||
- **最小权限**:Go 端用**只读** DB 账号;写权限仅 ingestion。
|
||||
- **采集端被封应对**:合规限速 + 失败退避 + 死信队列 + 切换为官方 dump/API。
|
||||
- **DDoS**:CDN + 速率限制 + 云厂商防护;公益服务以可降级(只读缓存)保命。
|
||||
- **依赖安全**:Go `govulncheck`、Python `pip-audit`,CI 中扫描。
|
||||
|
||||
---
|
||||
|
||||
## 8. 商品图片处理
|
||||
|
||||
- **版权**:OFF 图片为 CC-BY-SA,须署名 + 同样开放;逐图记 `license` 与来源。
|
||||
- **存储**:对象存储(MinIO/S3),路径按 `gtin/kind`;原图 + 生成多档缩略图(thumb/medium)。
|
||||
- **处理管线**:下载 → 校验(类型/大小) → 去重(感知哈希避免重复) → 压缩 → 生成缩略图 → 记录。
|
||||
- **分发**:CDN 加速;API 只返回图片 URL,不内嵌二进制。
|
||||
- **合规**:不展示含个人信息的图;提供权利方下架通道。
|
||||
- **降级**:图片缺失返回占位;图片服务故障不影响参数 API。
|
||||
|
||||
---
|
||||
|
||||
## 9. 可用性与 SLA
|
||||
|
||||
| 项 | 目标(建议) |
|
||||
|----|-----------|
|
||||
| API 可用性 | 99.5%(公益项目务实目标,先保只读可用) |
|
||||
| 读延迟 | p95 < 200ms(缓存命中 < 50ms) |
|
||||
| 数据新鲜度 | 增量同步 T+1(每日) |
|
||||
| 降级策略 | DB 故障 → 只读缓存兜底;图片/搜索故障不影响核心参数查询 |
|
||||
| 灾备 | 每日备份 + 异地副本;恢复演练季度一次 |
|
||||
| 维护窗口 | 采集/重建索引放低峰;API 滚动发布不停服 |
|
||||
|
||||
> 公益项目优先"省成本 + 稳定只读";写入(采集)可异步、可补偿,读路径要稳。
|
||||
|
||||
---
|
||||
|
||||
## 10. 竞品 / 同类项目分析
|
||||
|
||||
| 项目 | 性质 | 数据 | 借鉴点 | 与我们差异 |
|
||||
|------|------|------|--------|------------|
|
||||
| **Open Food Facts** | 公益食品库 | ODbL, 海量, 可贡献, 有 dump/API | 字段模型、众包、Nutri-Score、API 设计 | 我们多语言架构(Go API)、聚焦中文/GPC、收 MSRP |
|
||||
| **USDA FoodData Central** | 政府营养库 | CC0, 权威营养 | 营养数据补全、公共领域许可 | 偏美国/营养, 无条码生态 |
|
||||
| **GS1 / Verified by GS1** | 官方条码登记 | 受限, 权威, 2亿+ | 条码→品牌/规格权威源 | 非开放、需授权 |
|
||||
| **Wikidata** | 通用知识库 | CC0, 有 GTIN 属性(P3962) | 实体链接、结构化、开放 | 非商品专用、参数不规整 |
|
||||
| **schema.org Product/gtin** | 数据标准 | 标准而非数据 | 用其词汇做对外结构化(SEO/互操作) | 仅规范, 需我们填数据 |
|
||||
| **brocade.io / 各条码库** | 开放/商业条码库 | 参差 | 条码补全兜底 | 数据量/质量有限或收费 |
|
||||
|
||||
**结论与定位**:
|
||||
- 我们不是再造 OFF,而是做**面向中文世界、与 GS1 GPC 对齐、聚焦"商品参数标签"**的公益 API;
|
||||
- **站在巨人肩上**:OFF/USDA 做种子与营养,GS1 做条码权威,Wikidata/schema.org 做实体与互操作标准;
|
||||
- 差异化:中文优先、品类参数模板规整、官方 MSRP、字段级溯源、Go 高并发只读 API。
|
||||
|
||||
---
|
||||
|
||||
## 11. 小结
|
||||
v3.0 已把工程落地与公益合规的关键面全部展开。规划层面已相当完整。
|
||||
你之前说先不写代码,我**继续待命**:可以再深化任何一块,或等你说"开始",从 M0 搭骨架开 PR。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 天工·商品标签 (OpenGoods) — 规划文档归档
|
||||
|
||||
本目录归档了项目从立项到方案定稿的全部规划文档。
|
||||
|
||||
## 项目一句话
|
||||
公益网站/服务:**采集全网商品信息,对外提供商品参数查询 API**。
|
||||
核心原则:**只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。**
|
||||
|
||||
## 当前文档(最新,建议优先阅读)
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [00-final-plan.md](./00-final-plan.md) | **最终规划**:锁定的全部决策 + 架构 + 仓库结构 + M0~M5 可执行任务清单 |
|
||||
| [01-detailed-design-v2.0.md](./01-detailed-design-v2.0.md) | **详细设计**:商品分类体系(GS1 GPC)、单位管理、数据库设计、API 契约、数据治理、采集合规、部署运维、众包、里程碑估算 |
|
||||
| [02-advanced-topics-v3.0.md](./02-advanced-topics-v3.0.md) | **进阶专题**:完整 OpenAPI、全表 DDL、数据契约、OFF 字段映射、中国合规专项、测试、安全反滥用、图片处理、SLA、竞品分析 |
|
||||
|
||||
## 演进历史(History)
|
||||
|
||||
| 文档 | 阶段 |
|
||||
|------|------|
|
||||
| [history/v0.1-initial-plan.md](./history/v0.1-initial-plan.md) | 初版总体规划 |
|
||||
| [history/v0.2-go-python-foodfmcg.md](./history/v0.2-go-python-foodfmcg.md) | 确定 Go+Python 架构、聚焦食品快消、数据源调研 |
|
||||
| [history/v1.0-locked-decisions.md](./history/v1.0-locked-decisions.md) | 决策定稿(默认值) |
|
||||
|
||||
## 已锁定的关键决策(速览)
|
||||
|
||||
| 维度 | 决策 |
|
||||
|------|------|
|
||||
| 首批品类 | 食品快消 |
|
||||
| 价格 | 只收官方标准零售价 (MSRP),静态字段,无购买入口 |
|
||||
| 技术栈 | Go(对外只读 API) + Python(采集/ETL),经 PostgreSQL + Redis/队列解耦 |
|
||||
| 种子数据 | Open Food Facts 食品 dump |
|
||||
| 数据许可 | 对外 ODbL + 署名;CC0 来源(USDA)自由混入 |
|
||||
| 商品分类 | GS1 GPC 四层标准码 + 自建中文品类树映射 |
|
||||
| 单位管理 | 原始值+归一化双存;营养统一 per_100g/ml;能量双存 kJ+kcal |
|
||||
| 质量评分 | 0.4 完整度 + 0.3 来源权威 + 0.2 多源一致 + 0.1 新鲜度 |
|
||||
| 众包 | 一期不做,先纯采集;二期开放 |
|
||||
| Go 框架 / 迁移 / 部署 | chi + 标准库 / golang-migrate / Docker Compose |
|
||||
|
||||
> 注:文档中"中国合规专项"为工程与运营层面梳理,**非法律意见**;正式上线前请咨询专业法务。
|
||||
@@ -0,0 +1,216 @@
|
||||
# 商品档案公益 API 系统 — 规划方案 (v0.1)
|
||||
|
||||
> 一个公益性质的网站/服务:**采集全网商品信息**,对外提供**商品参数查询 API**。
|
||||
> 核心原则:**只收集信息、只提供信息,不涉及任何购买、下单、比价导购等交易行为。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目定位与原则
|
||||
|
||||
| 维度 | 说明 |
|
||||
|------|------|
|
||||
| 定位 | 公益的"商品参数百科 / 商品档案库",类似商品界的 Wikipedia + 开放 API |
|
||||
| 提供什么 | 商品的客观参数(规格、型号、成分、能效、尺寸、条码等) |
|
||||
| **不提供什么** | 价格交易、加购物车、下单、跳转购买链接、联盟分佣、比价导购 |
|
||||
| 服务对象 | 第三方软件 / 开发者,通过 API 查询商品参数 |
|
||||
| 数据态度 | 客观、中立、可溯源(每条数据标注来源与采集时间) |
|
||||
|
||||
> ⚠️ 关于价格:建议**默认不收录价格**。价格属于交易属性,且实时性强、争议大。如果一定要做,只做"历史参考价"且明确标注来源时间,绝不提供购买入口。**这一点需要你确认。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统总体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ 数据来源 (Sources) │
|
||||
│ 官网/厂商 / 开放数据 / GS1 │
|
||||
│ 条码库 / 用户贡献 / 监管公开 │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────┐
|
||||
│ 采集层 Ingestion (Workers) │
|
||||
│ 爬虫调度 + 适配器 + 限速 + robots 合规 + 去重 │
|
||||
└──────────────────────────┬──────────────────────────┘
|
||||
│ 原始数据 (raw)
|
||||
┌──────────────────────────▼──────────────────────────┐
|
||||
│ 清洗/标准化 ETL (Normalize & Dedup) │
|
||||
│ 字段映射 / 单位归一 / 实体匹配 / 质量评分 │
|
||||
└──────────────────────────┬──────────────────────────┘
|
||||
│ 结构化商品档案
|
||||
┌──────────────────────────▼──────────────────────────┐
|
||||
│ 存储层 Storage │
|
||||
│ PostgreSQL(主) + 对象存储(图片) + 搜索引擎(检索) │
|
||||
└──────────────────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────┐
|
||||
│ 公开 API 服务 (FastAPI) │
|
||||
│ REST/JSON + 文档 + 限流 + 缓存 + API Key(可选) │
|
||||
└──────────────────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────┐
|
||||
│ 消费者:各种软件/开发者 │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
分为四个相对独立的子系统:
|
||||
1. **采集子系统**(爬虫/适配器,离线运行)
|
||||
2. **数据处理子系统**(清洗、标准化、去重、质量评分)
|
||||
3. **存储子系统**(关系库 + 搜索 + 对象存储)
|
||||
4. **API 子系统**(对外只读公开 API + 文档站)
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心数据模型(商品档案 Schema)
|
||||
|
||||
商品的本质是"一个实体 + 一组可扩展的参数"。建议采用 **核心字段 + 灵活属性(KV)** 的混合模型,以适配不同品类(手机、食品、家电、化妆品……参数差异极大)。
|
||||
|
||||
### 3.1 核心实体
|
||||
|
||||
```jsonc
|
||||
// Product 商品档案
|
||||
{
|
||||
"id": "uuid", // 内部唯一ID
|
||||
"gtin": "6901234567892", // 全球贸易项目代码(条码), 可空
|
||||
"name": "示例牌 1.5L 纯净水",
|
||||
"brand": "示例牌", // -> Brand 实体
|
||||
"manufacturer": "示例食品有限公司",
|
||||
"category": "饮料/包装水", // -> Category 树
|
||||
"model": "型号/SKU标识",
|
||||
"description": "客观描述, 非营销文案",
|
||||
"images": ["对象存储URL", ...],
|
||||
"attributes": [ // 灵活参数(见下)
|
||||
{"key": "容量", "value": "1.5", "unit": "L"},
|
||||
{"key": "保质期", "value": "12", "unit": "月"}
|
||||
],
|
||||
"identifiers": { // 其他标识
|
||||
"ean": "...", "upc": "...", "asin": "...", "mpn": "..."
|
||||
},
|
||||
"sources": [ // 数据溯源(每个字段可标来源)
|
||||
{"source_id": "...", "url": "...", "fetched_at": "2026-06-08T...", "field": "容量"}
|
||||
],
|
||||
"quality_score": 0.87, // 数据质量/可信度评分
|
||||
"status": "active|merged|deprecated",
|
||||
"created_at": "...", "updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 灵活属性 (EAV / JSONB)
|
||||
- 不同品类参数差异巨大,核心表存通用字段,品类专属参数存 `attributes`(PostgreSQL `JSONB`,可建 GIN 索引)。
|
||||
- 配合**品类参数模板**(Category Schema)约束某品类应有哪些参数,保证质量。
|
||||
|
||||
### 3.3 辅助实体
|
||||
- `Brand`(品牌)、`Manufacturer`(厂商)、`Category`(品类树)、`Source`(数据来源登记)、`AttributeDefinition`(参数字典:标准名/别名/单位)。
|
||||
- 实体去重/合并需要 `merge` 机制(同一商品多来源 → 合并为一条,保留溯源)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据采集策略(最关键、也最需合规)
|
||||
|
||||
### 4.1 来源优先级(从"最合规"到"需谨慎")
|
||||
1. **官方开放数据 / 标准库**:GS1 条码库、各国监管公开数据(能效标识、食品备案、药品/化妆品备案等)。✅ 最佳
|
||||
2. **厂商官网 / 官方规格表**:参数最权威。需遵守 robots.txt。
|
||||
3. **厂商/平台开放 API**:若有官方 API 走 API。
|
||||
4. **用户/社区贡献**:众包补全与纠错(带审核)。
|
||||
5. **第三方网页抓取**:⚠️ 合规风险最高,需严格遵守 robots、限速、只取客观参数、标注来源。
|
||||
|
||||
### 4.2 采集器设计
|
||||
- **适配器模式**:每个来源一个 adapter(解析规则独立、可热插拔)。
|
||||
- **调度**:任务队列(Celery / RQ / arq)+ 定时(cron)+ 增量更新。
|
||||
- **合规护栏**:尊重 `robots.txt`、礼貌限速、`User-Agent` 标识身份、错峰、缓存避免重复抓取。
|
||||
- **去重与匹配**:以 GTIN/条码为主键,无条码时用 (品牌+型号+关键参数) 做模糊匹配。
|
||||
|
||||
### 4.3 数据质量
|
||||
- 每个字段记录来源 + 时间;多来源冲突时按来源可信度加权。
|
||||
- 质量评分 `quality_score`:字段完整度 + 来源权威度 + 一致性。
|
||||
|
||||
---
|
||||
|
||||
## 5. 公开 API 设计(只读、RESTful)
|
||||
|
||||
基础原则:**只读、无副作用、无购买入口、稳定版本化、有文档**。
|
||||
|
||||
```
|
||||
GET /api/v1/products/{id} # 按内部ID查询商品档案
|
||||
GET /api/v1/products/barcode/{gtin} # 按条码(GTIN/EAN/UPC)查询 ★最常用
|
||||
GET /api/v1/products/search # 搜索: ?q=&brand=&category=&page=&size=
|
||||
GET /api/v1/products/{id}/attributes # 仅取参数
|
||||
GET /api/v1/brands / categories # 品牌/品类树
|
||||
GET /api/v1/sources/{id} # 数据来源说明(透明溯源)
|
||||
GET /healthz / /api/v1/openapi.json # 健康检查 / 机读文档
|
||||
```
|
||||
|
||||
设计要点:
|
||||
- **版本化** `/api/v1/`,破坏性变更升 `v2`。
|
||||
- **分页 + 字段筛选**(`fields=` 减少传输)。
|
||||
- **限流**:匿名按 IP 限流;可选 API Key 提升配额(免费,仅用于防滥用与统计)。
|
||||
- **缓存**:CDN + 服务端缓存(商品参数变化慢,缓存命中率高)。
|
||||
- **响应统一**:JSON,含 `data` / `meta`(分页) / `sources`(溯源)。
|
||||
- **开放协议**:数据采用开放许可(如 CC BY / ODbL),鼓励署名引用。
|
||||
- **自动文档**:FastAPI 自带 Swagger UI / ReDoc。
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术选型建议
|
||||
|
||||
| 层 | 选型 | 理由 |
|
||||
|----|------|------|
|
||||
| API 框架 | **Python + FastAPI** | 与仓库定位一致、异步性能好、自带 OpenAPI 文档 |
|
||||
| 主数据库 | **PostgreSQL** (JSONB) | 关系 + 灵活属性兼得,GIN 索引支持检索 |
|
||||
| 搜索 | **OpenSearch / Elasticsearch / 或 PG 全文** | 商品名/参数全文与分面检索 |
|
||||
| 缓存 | **Redis** | 热点缓存 + 限流计数 + 任务队列后端 |
|
||||
| 采集任务 | **arq / Celery / RQ** | 异步调度爬虫与 ETL |
|
||||
| 爬虫 | **httpx + selectolax/BeautifulSoup**,动态页用 **Playwright** | 轻量为主,必要时浏览器渲染 |
|
||||
| 对象存储 | **S3 兼容 (MinIO / 云)** | 存商品图片 |
|
||||
| 部署 | **Docker + Compose**(初期) → K8s(规模化) | 渐进式 |
|
||||
| 文档站 | FastAPI 文档 + 静态站(MkDocs) | 开发者文档 |
|
||||
|
||||
> 如果你更偏好 Node.js / Go 也可以,我按你的偏好调整。仓库描述像是 FastAPI,所以我默认 Python。
|
||||
|
||||
---
|
||||
|
||||
## 7. 合规与法律(公益项目尤其重要)
|
||||
|
||||
- **爬取合规**:遵守 robots.txt、服务条款、合理限速;只采集**客观商品参数**,不抓取受版权保护的营销文案/评测原文(可链接来源)。
|
||||
- **数据来源透明**:每条数据可溯源,标注来源与时间,尊重原始来源。
|
||||
- **隐私**:只处理商品信息,不涉及个人数据(无 PII)。
|
||||
- **商标/品牌**:品牌名仅用于客观标识商品,不做背书或贬损。
|
||||
- **明确边界**:网站显著声明"仅提供信息、不提供购买、不构成消费建议"。
|
||||
- **数据开放许可**:选择 CC BY 4.0 或 ODbL,明确他人使用条款。
|
||||
- **下架机制**:提供来源方/权利方的纠错与下架联系渠道。
|
||||
|
||||
> 建议这块后续找法务/合规确认,我可以先把"合规护栏"写进采集器与站点声明。
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议的实施路线图(分阶段,先跑起来再扩展)
|
||||
|
||||
**阶段 0 — 地基(先做)**
|
||||
- 仓库工程化:FastAPI 项目骨架、PostgreSQL、Docker Compose、CI、代码规范。
|
||||
- 定义核心数据模型 + 迁移。
|
||||
|
||||
**阶段 1 — MVP API**
|
||||
- 实现只读 API:按条码/ID 查询 + 搜索 + 自动文档。
|
||||
- 导入一批种子数据(用开放数据/手工录入几百条),让 API 真正可用。
|
||||
|
||||
**阶段 2 — 采集管线**
|
||||
- 1~2 个合规来源的 adapter + 调度 + 清洗/去重/入库。
|
||||
- 质量评分与溯源。
|
||||
|
||||
**阶段 3 — 规模化与开放**
|
||||
- 搜索引擎、缓存/CDN、限流、API Key、众包贡献与审核后台。
|
||||
- 开放数据许可与开发者文档站上线。
|
||||
|
||||
---
|
||||
|
||||
## 9. 需要你拍板的关键问题
|
||||
|
||||
1. **是否收录价格?**(建议默认不收录,只做客观参数)
|
||||
2. **首批聚焦哪些品类?**(如:食品/快消、3C 数码、家电…… 建议先聚焦 1~2 个品类把模型打磨好)
|
||||
3. **技术栈确认 Python + FastAPI 吗?**(还是有其他偏好)
|
||||
4. **数据来源**:你手上有没有已知的合规数据源 / 官方 API / 已有数据集?
|
||||
5. **首要交付**:先搭"可查询的 MVP API + 一批种子数据",还是先做"采集管线"?(建议先 MVP API)
|
||||
|
||||
---
|
||||
|
||||
> 这是 v0.1 规划草案,确认大方向后我会细化成可执行的工程任务,并开始搭建仓库骨架 + MVP。
|
||||
@@ -0,0 +1,203 @@
|
||||
# 商品档案公益 API 系统 — 规划方案 (v0.2)
|
||||
|
||||
> 公益网站/服务:**采集全网商品信息**,对外提供**商品参数查询 API**。
|
||||
> 原则:**只收集 + 只提供信息,不涉及任何购买/下单/比价导购**。
|
||||
> 本版根据你的反馈定稿四件事:① 收录**官方标准零售价(MSRP)** ② 首批聚焦**食品快消** ③ **Go(系统) + Python(采集)** 多语言架构 ④ 附**开放数据源清单**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 你已确认的决策
|
||||
|
||||
| # | 决策 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | **价格 = 官方标准零售价 (MSRP)** | 厂商指导价/官方建议零售价,属**静态属性**,带来源+时间+币种标注;**不收录实时电商售价、不提供购买入口** |
|
||||
| 2 | **首批品类 = 食品快消 (Food & FMCG)** | 先把食品的数据模型打磨好(成分、营养、过敏原、规格、保质期…) |
|
||||
| 3 | **技术栈 = Go + Python** | Go 写对外 API/核心服务;Python 写采集/ETL/爬虫;通过 PostgreSQL + 消息队列解耦 |
|
||||
| 4 | **数据源 = 暂无,后期提供** | 本版先给出可立即接入的开放数据源清单 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Go + Python 多语言架构(核心)
|
||||
|
||||
这是一个很经典且合理的组合。两端**不直接互相调用**,而是通过**共享数据库 + 消息队列**解耦,各自独立部署、独立扩展。
|
||||
|
||||
```
|
||||
┌────────────────────── Python 侧 (采集/数据) ──────────────────────┐
|
||||
│ │
|
||||
数据源 ─▶│ 采集 Workers (爬虫/适配器) ─▶ ETL 清洗/标准化/去重 ─▶ 入库 │
|
||||
│ httpx / Playwright / scrapy pandas / 规则引擎 │
|
||||
└───────────────────────────┬────────────────────────────────────────┘
|
||||
│ 写入
|
||||
┌───────▼────────┐ ┌──────────────┐
|
||||
│ PostgreSQL │◀──────▶│ 对象存储 S3 │ (商品图)
|
||||
│ (商品档案主库) │ └──────────────┘
|
||||
└───────▲────────┘
|
||||
│ 只读
|
||||
┌───────────────────────────┴────────────────────────────────────────┐
|
||||
│ Go 侧 (对外服务) │
|
||||
│ 公开 API (REST/JSON) + Redis 缓存/限流 + 搜索网关 + OpenAPI │
|
||||
│ Gin/Echo/Chi/标准库 │
|
||||
└───────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
各种软件 / 开发者消费
|
||||
```
|
||||
|
||||
### 1.1 职责划分
|
||||
|
||||
| 子系统 | 语言 | 职责 |
|
||||
|--------|------|------|
|
||||
| **公开 API 服务** | **Go** | 对外只读 API、限流、缓存、鉴权(可选 API Key)、检索网关、高并发承载 |
|
||||
| **采集 Workers** | **Python** | 每个数据源一个 adapter,抓取/调用 API、遵守 robots、限速、产出原始数据 |
|
||||
| **ETL / 数据处理** | **Python** | 清洗、字段映射、单位归一、实体去重与合并、质量评分 |
|
||||
| **调度 / 队列** | Python(worker) + Redis/消息队列 | 定时任务、增量更新、任务分发 |
|
||||
| **存储** | PostgreSQL + Redis + S3 | 主库 / 缓存+限流 / 图片 |
|
||||
| **检索** | 初期 PG 全文 → 后期 OpenSearch | 商品名/参数搜索与分面 |
|
||||
|
||||
### 1.2 为什么这样分?
|
||||
- **Go 做 API**:编译型、单二进制部署、并发模型适合高 QPS 的只读公益 API,运维简单。
|
||||
- **Python 做采集**:爬虫/解析/数据处理生态最强(scrapy、playwright、pandas),迭代快。
|
||||
- **解耦点 = 数据库**:Go 端**只读**主库(或读副本),Python 端负责写入。两端通过稳定的表结构约定协作,互不阻塞;将来任一端换语言/重写都不影响另一端。
|
||||
- **契约**:用数据库 schema + 一份内部「数据契约文档」固定字段含义,避免两端理解不一致。
|
||||
|
||||
---
|
||||
|
||||
## 2. 食品快消数据模型(细化)
|
||||
|
||||
食品参数差异大,沿用 **核心字段 + JSONB 灵活属性 + 营养结构化子表**。字段设计大量参考 Open Food Facts(成熟的食品开放库)。
|
||||
|
||||
### 2.1 商品主表 `product`
|
||||
```jsonc
|
||||
{
|
||||
"id": "uuid",
|
||||
"gtin": "6901234567892", // 条码(主键标识), EAN-13/UPC/EAN-8
|
||||
"name": "示例牌 巧克力榛子酱 400g",
|
||||
"brand": "示例牌", // -> brand
|
||||
"manufacturer": "示例食品有限公司", // 生产商
|
||||
"category": "食品/酱料/巧克力酱", // -> category 树 (可对齐 GS1 GPC / OFF categories)
|
||||
"net_content": {"value": 400, "unit": "g"}, // 净含量
|
||||
"country_of_origin": "中国",
|
||||
"shelf_life": {"value": 12, "unit": "月"}, // 保质期
|
||||
"storage": "常温避光保存",
|
||||
"images": ["S3_URL", ...],
|
||||
"msrp": { ... }, // 官方标准零售价, 见 2.3
|
||||
"food": { ... }, // 食品专属结构化字段, 见 2.2
|
||||
"attributes": [ {"key":"","value":"","unit":""} ], // 其余灵活参数(JSONB)
|
||||
"identifiers": {"ean":"", "upc":"", "off_id":""},
|
||||
"sources": [ {"source":"", "url":"", "fetched_at":"", "fields":["msrp"]} ],
|
||||
"quality_score": 0.0,
|
||||
"status": "active|merged|deprecated",
|
||||
"created_at": "", "updated_at": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 食品专属字段 `food`(结构化)
|
||||
```jsonc
|
||||
{
|
||||
"ingredients_text": "白砂糖, 棕榈油, 榛子(13%), ...", // 配料表原文
|
||||
"ingredients": [ {"name":"白砂糖","rank":1}, ... ], // 解析后(可选)
|
||||
"allergens": ["坚果", "大豆", "乳"], // 过敏原
|
||||
"additives": ["E322 卵磷脂"], // 添加剂
|
||||
"nutriments": { // 营养成分(每100g/100ml)
|
||||
"energy_kj": 2252, "energy_kcal": 539,
|
||||
"fat_g": 30.9, "saturated_fat_g": 10.6,
|
||||
"carbohydrates_g": 57.5, "sugars_g": 56.3,
|
||||
"protein_g": 6.3, "salt_g": 0.107
|
||||
},
|
||||
"nutrition_basis": "per_100g", // per_100g | per_100ml | per_serving
|
||||
"serving_size": "15g",
|
||||
"is_vegetarian": null, "is_vegan": null, // 可空
|
||||
"nutri_score": "C", // 若引用 OFF
|
||||
"labels": ["无添加", "清真"] // 认证/标签
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 官方标准零售价 `msrp`(重点)
|
||||
```jsonc
|
||||
{
|
||||
"amount": 29.90,
|
||||
"currency": "CNY",
|
||||
"type": "msrp", // 仅 msrp/官方指导价; 不存实时电商成交价
|
||||
"region": "CN", // 适用地区(价格随地区不同)
|
||||
"source": "厂商官网/官方价目表",
|
||||
"source_url": "https://...",
|
||||
"effective_date": "2026-01-01", // 价格生效/采集时间
|
||||
"note": "官方建议零售价, 实际售价以零售商为准; 本站不提供购买"
|
||||
}
|
||||
```
|
||||
> 设计要点:价格是**带时间戳的历史快照**而非实时报价;明确 `type=msrp`、标注地区与来源;响应里附免责说明。**坚决不出现购买/跳转链接。**
|
||||
|
||||
### 2.4 辅助实体
|
||||
`brand` / `manufacturer` / `category`(品类树) / `source`(数据来源登记) / `attribute_definition`(参数字典: 标准名·别名·单位) / `merge_log`(实体合并记录, 保留溯源)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 可立即接入的开放数据源清单(食品快消)
|
||||
|
||||
按"合规性 / 可用性"排序。这些可作为**种子数据 + 采集 adapter 的首批对象**。
|
||||
|
||||
| 数据源 | 内容 | 许可 | 接入方式 | 备注 |
|
||||
|--------|------|------|----------|------|
|
||||
| **Open Food Facts** ⭐ | 全球食品(成分/营养/过敏原/Nutri-Score/图片) | **ODbL**(数据)+DbCL+CC-BY-SA(图) | REST API + **每夜全量 dump**(CSV/MongoDB, ~9GB) | 食品首选;可贡献回写;限速 15 req/min/IP(读) |
|
||||
| **USDA FoodData Central** ⭐ | 美国食品营养成分(含 Branded 品牌库) | **CC0(公共领域)** | REST API(需免费 key) + JSON/CSV 下载 | 营养数据权威;商业可用 |
|
||||
| **GS1 / Verified by GS1**(中国商品信息服务平台) | 条码→品牌/规格/厂商(官方登记) | 受限(需企业/接口授权) | 网页查询 + API(≤1000 GTIN/次) | **条码→商品**最权威来源;2亿+条;中国数据首选 |
|
||||
| **brocade.io** | 开放 GTIN/条码产品库 | 开源/开放 | 免费 REST(免鉴权读) | 数据量有限,可作补充 |
|
||||
| **3023data 等条码接口** | 中国物品编码+UPC+ISBN | 商业(0.005~0.02元/次) | REST API | **付费**,作兜底补全,非首选 |
|
||||
| 各国**监管公开数据** | 食品备案/标签/能效等 | 多为公开 | 各平台 | 后续按需逐个评估合规 |
|
||||
|
||||
**参考用开源项目(架构/数据模型借鉴,非数据源)**:
|
||||
- Open Food Facts Server (Product Opener) — 食品库的完整实现,可学其字段与流程
|
||||
- UnoPIM / PCMT / brocade.io — 开源 PIM / 商品主数据系统,借鉴建模与去重
|
||||
|
||||
> 建议:**先用 Open Food Facts 全量 dump 作种子数据**(直接有海量真实食品),再用 GS1/USDA 做补全与校验。这样 MVP 阶段就有真实可查的数据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 公开 API 契约(Go 实现,只读)
|
||||
|
||||
```
|
||||
GET /api/v1/products/barcode/{gtin} # ★最常用: 条码查档案
|
||||
GET /api/v1/products/{id} # 内部ID查
|
||||
GET /api/v1/products/search # ?q=&brand=&category=&allergen_free=&page=&size=&fields=
|
||||
GET /api/v1/products/{id}/nutriments # 仅营养
|
||||
GET /api/v1/products/{id}/msrp # 仅官方零售价(含来源/时间/免责)
|
||||
GET /api/v1/brands | /categories # 品牌 / 品类树
|
||||
GET /api/v1/sources/{id} # 数据来源透明说明
|
||||
GET /healthz | /api/v1/openapi.json # 健康检查 / 机读文档
|
||||
```
|
||||
约定:版本化 `/v1/`;统一响应 `{data, meta(分页), sources(溯源)}`;分页 + `fields=` 裁剪;匿名按 IP 限流,可选免费 API Key 提配额;CDN+Redis 缓存(参数变化慢,命中率高);数据采用开放许可(CC BY / ODbL,注意 OFF 的 ODbL 传染性);**无任何购买/交易端点**。
|
||||
|
||||
---
|
||||
|
||||
## 5. 合规与边界(公益项目重点)
|
||||
|
||||
- **数据源许可要分清**:OFF 是 **ODbL**(衍生数据库需同样开放+署名),USDA 是 **CC0**(最宽松)。混用时要按最严格许可对外标注,避免许可冲突。
|
||||
- 爬取守 robots.txt / 服务条款,礼貌限速,标明 User-Agent 身份。
|
||||
- 只采**客观参数**;营销文案/评测原文不照搬(链接来源即可)。
|
||||
- 无个人数据(PII),只处理商品信息。
|
||||
- 站点显著声明:**仅提供信息、不提供购买、不构成消费建议**;价格为官方指导价历史快照。
|
||||
- 提供权利方**纠错/下架**联系渠道。
|
||||
|
||||
---
|
||||
|
||||
## 6. 里程碑(仍不写代码,仅规划,供确认)
|
||||
|
||||
| 阶段 | 目标 | 关键产出 |
|
||||
|------|------|----------|
|
||||
| **M0 工程地基** | 仓库骨架 | Go API 骨架 + Python 采集骨架 + PostgreSQL + Docker Compose + CI + 数据契约文档 |
|
||||
| **M1 数据模型** | 食品 schema | 主表/食品字段/MSRP/辅助实体 的迁移与字典 |
|
||||
| **M2 种子数据** | 有真实数据 | 导入 Open Food Facts dump(食品子集) + USDA 营养补全 |
|
||||
| **M3 MVP API (Go)** | 可查询 | 条码/ID/搜索/营养/MSRP + OpenAPI 文档 + 限流缓存 |
|
||||
| **M4 采集管线 (Python)** | 自动更新 | 1~2 个 adapter(OFF API / GS1) + ETL + 去重 + 质量评分 + 调度 |
|
||||
| **M5 开放与规模化** | 上线 | 搜索引擎 + CDN + API Key + 众包纠错后台 + 开发者文档站 + 开放数据许可 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 待你确认/补充
|
||||
|
||||
1. **价格范围**:确认只收「官方指导价 (MSRP)」、不碰实时电商价?(建议是)
|
||||
2. **OFF 的 ODbL 许可**:可接受(意味着我们对外的数据库也要用 ODbL 并署名 OFF)?还是更想用 CC0 来源(USDA)为主以保持宽松?
|
||||
3. **种子数据**:同意先导入 Open Food Facts 食品 dump 作为启动数据吗?
|
||||
4. **Go Web 框架偏好**:Gin / Echo / Chi / 标准库 net/http,有偏好吗?(无偏好我默认 Chi 或标准库,轻量)
|
||||
5. **地域范围**:首批面向中国市场商品,还是中外都收?(影响优先用 GS1-China 还是 OFF 全球库)
|
||||
|
||||
> 你确认后,我把它定为 v1.0 规划,并据此拆成可执行的工程任务清单(仍按你的节奏,需要我动手写代码时再开始)。
|
||||
@@ -0,0 +1,107 @@
|
||||
# 商品档案公益 API 系统 — 规划方案 (v1.0 定稿)
|
||||
|
||||
> 公益网站/服务:采集全网商品信息,提供商品参数查询 API。**只收集 + 只提供信息,不涉及任何购买行为。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 已锁定的决策
|
||||
|
||||
| # | 决策 | 结论 |
|
||||
|---|------|------|
|
||||
| 1 | 品类 | 首批 **食品快消** |
|
||||
| 2 | 价格 | 只收 **官方标准零售价 (MSRP)**,静态字段,带来源/时间/地区/币种 + 免责说明;**不收实时电商价、不提供购买入口** |
|
||||
| 3 | 技术栈 | **Go**(对外API/核心服务) + **Python**(采集/ETL/爬虫),经 PostgreSQL + Redis/队列解耦 |
|
||||
| 4 | 种子数据 | ✅ **先导入 Open Food Facts 食品 dump**,最快拥有真实数据 |
|
||||
| 5 | 数据许可 | 因采用 OFF → 对外数据库用 **ODbL** 并署名来源;CC0 来源(USDA)可自由混入 |
|
||||
|
||||
### 1.1 我先用的默认值(如不同意请指出,否则按此执行)
|
||||
- **Go Web 框架**:`chi` + 标准库 `net/http`(轻量、稳定、易维护)。
|
||||
- **地域范围**:先用 OFF **全球食品库**起步,后续接 **GS1-China** 补强中国市场数据。
|
||||
- **数据库迁移工具**:Go 侧用 `golang-migrate`(纯 SQL 迁移,两端共享同一套 schema)。
|
||||
- **部署**:初期 Docker Compose 一键起全套(Postgres/Redis/Go API/Python worker)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标架构(定稿)
|
||||
|
||||
```
|
||||
数据源(OFF dump / OFF API / USDA / GS1)
|
||||
│
|
||||
▼ Python: 采集 adapters → ETL(清洗/归一/去重/质量评分)
|
||||
│
|
||||
┌────▼─────────┐ 图片 ┌──────────┐
|
||||
│ PostgreSQL │◀───────▶│ S3/MinIO │
|
||||
│ (商品档案主库)│ └──────────┘
|
||||
└────▲─────────┘
|
||||
│ 只读 (+Redis缓存/限流)
|
||||
▼ Go: 公开 REST API + OpenAPI 文档
|
||||
各种软件 / 开发者
|
||||
```
|
||||
|
||||
- **解耦契约**:两端通过共享 PostgreSQL schema + 一份《数据契约文档》协作,互不直接调用。
|
||||
- **Go 端只读主库**(或读副本);**Python 端负责写入**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 仓库结构(计划,写代码时落地)
|
||||
|
||||
```
|
||||
goods/
|
||||
├── README.md
|
||||
├── docker-compose.yml # postgres + redis + minio + api + worker
|
||||
├── docs/
|
||||
│ ├── data-contract.md # 两端共享的字段契约
|
||||
│ └── openapi.yaml # API 契约
|
||||
├── migrations/ # 共享 SQL 迁移 (golang-migrate)
|
||||
├── api/ # Go: 对外只读 API
|
||||
│ ├── cmd/server/main.go
|
||||
│ ├── internal/{handler,store,model,middleware}/
|
||||
│ └── go.mod
|
||||
└── ingestion/ # Python: 采集 + ETL
|
||||
├── pyproject.toml
|
||||
├── adapters/{openfoodfacts,usda,gs1}.py
|
||||
├── etl/{normalize,dedup,quality}.py
|
||||
└── jobs/{seed_off_dump,scheduler}.py
|
||||
```
|
||||
|
||||
## 4. 数据模型 & API 契约
|
||||
(沿用 v0.2:`product` 主表 + `food` 食品字段 + `msrp` 价格 + 辅助实体;API 以 `GET /products/barcode/{gtin}` 为核心,全只读、无交易端点。详见 v0.2 附件。)
|
||||
|
||||
---
|
||||
|
||||
## 5. 可执行任务拆分(按里程碑,写代码时逐项落地)
|
||||
|
||||
**M0 — 工程地基**
|
||||
- [ ] 初始化 Go module (`api/`) + Python 项目 (`ingestion/`)
|
||||
- [ ] `docker-compose.yml`:Postgres + Redis + MinIO
|
||||
- [ ] CI(Go: build/vet/test;Python: ruff/pytest)
|
||||
- [ ] `docs/data-contract.md` 初版
|
||||
|
||||
**M1 — 数据模型**
|
||||
- [ ] `migrations/`:product / food / msrp / brand / manufacturer / category / source / attribute_definition / merge_log
|
||||
- [ ] JSONB + GIN 索引;gtin 唯一索引
|
||||
|
||||
**M2 — 种子数据 (Python)**
|
||||
- [ ] 下载 OFF 食品 dump(CSV)
|
||||
- [ ] `seed_off_dump`:字段映射 → 入库(含营养/成分/过敏原/图片URL)
|
||||
- [ ] USDA(CC0) 营养补全(可选)
|
||||
|
||||
**M3 — MVP API (Go)**
|
||||
- [ ] 路由 + handler:barcode / id / search / nutriments / msrp / brands / categories / sources
|
||||
- [ ] 统一响应、分页、`fields=` 裁剪、错误处理
|
||||
- [ ] Redis 缓存 + IP 限流;`/healthz` + OpenAPI 文档
|
||||
|
||||
**M4 — 采集管线 (Python)**
|
||||
- [ ] adapter:OFF API(增量更新)+ GS1(条码补全)
|
||||
- [ ] ETL:清洗/单位归一/去重合并/质量评分/溯源
|
||||
- [ ] 调度(定时增量更新)
|
||||
|
||||
**M5 — 开放与规模化**
|
||||
- [ ] 搜索引擎(PG 全文 → OpenSearch)、CDN 缓存
|
||||
- [ ] 免费 API Key(防滥用+统计)、众包纠错后台
|
||||
- [ ] 开发者文档站 + 开放数据许可声明 + 站点"不提供购买"声明
|
||||
|
||||
---
|
||||
|
||||
## 6. 下一步
|
||||
规划已定稿。**你说先不写代码,所以我暂停在这里**。等你说"开始",我就从 **M0 工程地基** 动手,搭好骨架后开 PR 给你看。也可以先只做某个里程碑(比如先 M0+M1 把骨架和数据模型立起来)。
|
||||
Reference in New Issue
Block a user